merge(0923): harita en üstte, üniversite tür çipi, URAP sırası, risk etiketi, ölçüm delikleri, unitur veri düzeltmesi
All checks were successful
Deploy / deploy (push) Successful in 6m40s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-23 11:36:59 +03:00
29 changed files with 1188 additions and 79 deletions

Binary file not shown.

View File

@@ -0,0 +1,68 @@
# 07 — P1: Riskin adı satırda yazsın
Dal: `gece/0922-risk-etiketi` · Ajan: `yazilimci` · Kapsam: CEO kararları §3 / P1
---
## Özet (5 madde)
1. `/sonuc` program tablosunda risk göstergesi artık **renkli nokta + yazılı ad**. Etiketin kaynağı `RISK_ETIKET` (`src/lib/risk.ts`); `risk.ts`'e dokunulmadı.
2. `/listem` (rapor listesi) risk etiketindeki `sr-only sm:not-sr-only` kaldırıldı — etiket artık 390px dahil her genişlikte görünüyor.
3. Yeni bileşen, yeni sabit, yeni olay yok. İki dosya değişti, toplam +21/−12 satır.
4. `/sonuc` tablosunda **boş olan ilk sütun başlığına "Risk" yazıldı** — şartnamede açıkça yazmıyordu, sapma olarak aşağıda gerekçelendirildi.
5. `next typegen` + `tsc --noEmit` + `lint` temiz (çalıştırılarak doğrulandı). Build alınmadı (iş tanımı istemiyordu), tarayıcıda doğrulama yapılmadı (AGENTS.md).
---
## Ne değişti
### `src/features/liste/components/program-tablosu.tsx`
- `RISK_ETIKET` import'a eklendi.
- Satırın ilk hücresi (`ProgramSatiri`, eski 306-314): `block size-2.5 … aria-hidden` olan tek nokta yerine,
`rapor-listesi.tsx:262-270` kalıbının birebir aynısı: `inline-flex items-center gap-1.5` kabında
`size-2.5 shrink-0 rounded-full` nokta (`aria-hidden`) + yanında `{RISK_ETIKET[risk]}`.
Hücreye `text-xs font-medium` verildi — rapor listesindeki risk hücresiyle aynı tipografi.
- Tablo başlığı: `<TableHead className="w-8" />` → `<TableHead className="w-8 pl-4 pr-0">Risk</TableHead>`.
- `RISK_NOKTA`'nın üstüne renk dili yorumu eklendi (rapor-listesi'ndeki yorumun eşi).
### `src/features/rapor/components/rapor-listesi.tsx`
- `:267` `<span className="sr-only sm:not-sr-only">` → `<span>`; etiket her zaman görünür.
- `RISK_STIL` üstündeki yorumdaki "dar ekranda sr-only olarak kalır" cümlesi gerçeğe göre düzeltildi.
Başka dosyaya dokunulmadı. `src/lib/risk.ts` değişmedi.
---
## Kabul kriterleri
| # | Kriter | Durum | Nasıl doğrulandı |
|---|---|---|---|
| 1 | `/sonuc` ve `/listem` satırlarında risk **yazıyla** görünür, kaynak `RISK_ETIKET` | ✔ | Koddan: `program-tablosu.tsx` risk hücresi `{RISK_ETIKET[risk]}` basıyor; `rapor-listesi.tsx`'te etiketi gizleyen tek sınıf (`sr-only`) kalktı. `grep -n "sr-only" ` ile risk hücresinde kalıntı yok |
| 2 | 390px'te etiket kesilmez, satır taşmaz | ✔ (koddan çıkarım — tarayıcıda bakılmadı) | `TableCell` varsayılanı zaten `whitespace-nowrap`, yani etiket kesilmez/sarmalanmaz. Program hücresi `max-w-0 w-full` + `truncate` olduğu için artan genişliği o sütun yutar, tablo yatay taşmaz. Genişlik bedeli aşağıda |
| 3 | `RISK_NOKTA` rengi korunur ama tek başına anlam taşımaz | ✔ | `RISK_NOKTA` haritası aynen duruyor; nokta `aria-hidden` kaldı, anlamı taşıyan artık yanındaki metin |
| 4 | `tsc` + `lint` temiz | ✔ | `pnpm exec next typegen` → "Types generated successfully"; `pnpm exec tsc --noEmit` → çıkış 0, hiç çıktı yok; `pnpm lint` → hiç çıktı yok |
---
## Şartnameden sapmalar (2 adet, ikisi de küçük)
1. **`/sonuc` tablosunun ilk sütun başlığına "Risk" yazıldı.** Şartname yalnız satırdan söz ediyordu. Gerekçe: o sütun artık ikonik değil metinsel içerik taşıyor; başlığı boş bırakmak hem `rapor-listesi.tsx`'teki eşdeğer sütunla (orada başlık "Risk") çelişirdi hem de başlıksız metin sütunu bırakırdı. Genişlik bedeli yok — başlık ("Risk", ~30px) hücre içeriğinden dar, sütunu genişletmiyor. Başlığa `pl-4 pr-0` verildi ki hücrelerle hizalansın (yine `rapor-listesi.tsx:380`'deki `#` başlığının deseni).
2. **Tablonun yer darlığı için icat yapılmadı**, şartnamedeki yol izlendi: mevcut sütun yapısı korundu, etiket noktanın yanına `text-xs` (rapor listesiyle aynı punto) ile kondu. Daha küçük punto (`text-[10px]`) denenmedi çünkü kazancı ~5px, bedeli ise AGENTS.md'nin "aynı eleman iki yerde birebir aynı olsun" kuralından sapmak olurdu.
---
## Bilal'in gözle bakması gereken (tek madde)
**`/sonuc` tablosu 390px'te.** Risk sütunu artık ~86px yer kaplıyor (16px sol boşluk + 10px nokta + 6px boşluk + en uzun etiket "Az riskli" ≈ 55px). Bu genişliği program adı sütunu veriyor; o sütun `truncate` olduğu için satır taşmıyor ama **program adı daha erken kısalıyor** (kabaca 150px'ten ~85px'e; üniversite logosu da aynı hücrede). Kesilme yok, kısalma var. Tarayıcıda doğrulanmadı (AGENTS.md gereği açmadım) — telefonda bir bakış yeterli. Dar gelirse iki ucuz seçenek var, ikisi de ayrı karar: (a) etiketi `text-[10px]`e düşür, (b) `Kontenjan` sütununun `sm` eşiğini yükselt.
---
## Merge sonrası doğrulanması gerekenler
- `/sonuc` üç dilim sekmesinde de (hayal/dengeli/güvenli) etiket doğru kelimeyi yazıyor mu; taban verisi olmayan satırda (risk `null`) hücre boş kalıyor mu.
- `/listem` mobilde risk sütunu: kilitli satırlarda hücre içeriği hâlâ yalnız asma kilit ikonu (aşağıdaki nota bakın).
## Kapsam dışı bıraktıklarım (dokunmadım, sahibi ben değilim / şartname dışı)
- **`rapor-listesi.tsx:264` "Pakette" etiketi hâlâ `sr-only sm:not-sr-only`.** Yani mobilde kilitli satırın risk hücresinde yalnız asma kilit ikonu görünüyor. Bu bir renk ihlali değil (ikon, renk değil) ve şartname yalnız `:267`'yi kapsıyordu, o yüzden dokunmadım. Ama artık aynı sütunda bir satır "Az riskli" yazarken diğerinin sadece ikon göstermesi tutarsız duruyor; sütun genişliği zaten risk etiketine göre belirlendiği için bunu görünür yapmanın **ek genişlik maliyeti yok**. Tek satırlık iş, ayrı karar.
- **`liste-cekmecesi.tsx:426`** — çekmecedeki tercih satırlarında risk yalnız renkli nokta (`aria-label` var, görünür metin yok). Aynı çekmecenin başlığında renk dilini açıklayan yazılı bir açıklama listesi (`:172-184`) olduğu için tam ihlal sayılmaz; yine de bu, risk renginin metinsiz göründüğü **üçüncü** yüzey. Şartnamede yok, dosya sahibi değilim — backlog adayı.

View File

@@ -0,0 +1,72 @@
# P4 — İki ölçüm deliği · `gece/0922-olcum`
**Tur 3 · yazılımcı raporu · 2026-09-23**
## Özet (5 madde)
1. `sihirbaz_adim{adim:1}` artık sihirbaz modalı açıldığında (bileşenin mount'unda) bir kez atılıyor; huninin paydası `sira_girildi`den türetilmek zorunda değil.
2. Tekrar sayma koruması iki katmanlı: bileşen 1↔2 gezinmesinde unmount olmuyor (mount effect tekrar koşmuyor) **ve** `olayBirKez` oturum başına tekilleştiriyor.
3. Kaynaksız 10 `/paket` linkinin hepsine tekil `?kaynak=` eklendi; sitedeki 14 `/paket` linkinin 14'ünde artık kaynak var.
4. **Şartname dışı zorunlu dosya:** `paket-gorunum-olayi.tsx`'teki `KAYNAKLAR` allowlist'i genişletildi — genişletilmeseydi 10 yeni değerin tamamı Rybbit'e `"diger"` olarak düşerdi, yani iş ölçüm açısından işlevsiz kalırdı.
5. Görsel değişiklik yok: tüm diff ya yorum, ya `href` query string'i, ya da görünmez bir `useEffect`. `tsc --noEmit` ve `lint` temiz.
## Değişen dosyalar
| Dosya | Ne |
|---|---|
| `src/features/sihirbaz/components/sihirbaz-adimlar.tsx` | Mount'ta `olayBirKez("sihirbaz_adim", { adim: 1 }, "adim1")`; `adimaGit` yorumu güncellendi (adım 1 artık oradan geçmiyor) |
| `src/app/page.tsx` | 2 link |
| `src/app/kosullar/page.tsx` | 1 link |
| `src/features/rapor/components/tercih-degisti-modali.tsx` | 2 link |
| `src/features/rapor/components/sohbet-client.tsx` | 3 link |
| `src/features/odeme/components/odeme-kromu.tsx` | 1 link |
| `src/features/odeme/components/odeme-sonuc-karti.tsx` | 1 link |
| `src/features/odeme/components/paket-gorunum-olayi.tsx` | **(şartname dışı)** `KAYNAKLAR` allowlist'i 3 → 13 değer |
## Link → kaynak adı tablosu
| # | Dosya:satır | Yüzey (kullanıcı ne görüyor) | `?kaynak=` |
|---|---|---|---|
| 1 | `src/app/page.tsx:543` | Ana sayfa fiyat bölümü, paket kartının CTA'sı ("… TL — Paketi incele") | `fiyat_paket` |
| 2 | `src/app/page.tsx:573` | Ana sayfa fiyat bölümü, "Kredi bitti mi?" topup kartı | `fiyat_kredi` |
| 3 | `src/app/kosullar/page.tsx:55` | Koşullar · Ücretlendirme paragrafındaki "paket sayfasında" linki | `kosullar` |
| 4 | `tercih-degisti-modali.tsx:283` | "Tercihlerin değişmiş" modalı, aksiyon satırı (kredi baştan yetmiyor) | `tercih_degisti` |
| 5 | `tercih-degisti-modali.tsx:335` | Aynı modal, `durum.ad === "kredi-yetersiz"` satırı (güncelleme denendi, kredi yetmedi) | `tercih_degisti_yetersiz` |
| 6 | `sohbet-client.tsx:351` | Danışman başlığındaki "N kredi" rozeti | `danisman_rozet` |
| 7 | `sohbet-client.tsx:538` | Danışman, kredi bitti bandındaki "Kredi yükle" butonu | `danisman_kredi_bitti` |
| 8 | `sohbet-client.tsx:546` | Danışman, "son N kredin" uyarısındaki "kredi yükleyip" linki | `danisman_kredi_az` |
| 9 | `odeme-kromu.tsx:45` | `/odeme` üst şeridi, "Paket sayfasına dön" | `odeme_geri` |
| 10 | `odeme-sonuc-karti.tsx:141` | Ödeme başarısız kartı, "Tekrar dene" | `odeme_hata` |
Adlandırma dili mevcut şemadan alındı (`kilit`, `kredi_bitti`, `nav`): küçük harf, `snake_case`, yüzeyi/tetiği söyleyen kısa ad. Yeni kavram icat edilmedi; `fiyat_*` ve `danisman_*` önekleri aynı bölümdeki iki linki ayırmak için.
## Kabul kriterleri
| # | Kriter | Durum | Nasıl doğrulandı |
|---|---|---|---|
| 1 | Sihirbaz açılışında `sihirbaz_adim{adim:1}` tam bir kez düşer; 1→2→1→2'de tekrar düşmez | ✔ (koddan çıkarım) | Modal `Dialog`/`DialogContent` içinde, `forceMount` yok → kapanınca `SihirbazAdimlar` unmount oluyor; `adim` bileşenin iç state'i, adım değişimi unmount etmiyor → boş bağımlılıklı `useEffect` bir kez koşuyor. Ek olarak `olayBirKez` sessionStorage anahtarıyla (`kolaytercih.olay.sihirbaz_adim.adim1`) oturum başına tekilleştiriyor; bu aynı zamanda dev StrictMode'un çift effect'ini ve `hero-form`daki `key={sira-tur}` remount'unu da kapatıyor. **Tarayıcıda çalıştırılarak doğrulanmadı** (vardiya kuralı: dev sunucusu/tarayıcı yok). |
| 2 | 14 `/paket` linkinin 14'ünde `?kaynak=` var ve değerler tekil | ✔ | `grep -rn 'href="/paket' src \| grep -v 'kaynak='` → boş. 10 yeni değerin hepsi birbirinden ve mevcut 3 değerden farklı. **Uyarı:** `listem-govde.tsx:288` ve `:369` ikisi de eskiden beri `kaynak=kilit` (iki ayrı kilit yüzeyi, tek değer) — bu benim paketimde değil, aşağıda not olarak duruyor. |
| 3 | Hiçbir görsel değişiklik yok | ✔ | `git diff`: dokunulan satırlar yalnız yorum, `href` string'i ve `null` döndürmeyen hiçbir JSX eklemeyen bir `useEffect`. Hiçbir `className`, metin, ikon, düzen değişmedi. |
| 4 | Commit mesajında seri kırılması notu | ✔ | Commit gövdesinde; ayrıca `sihirbaz-adimlar.tsx` içindeki yorumda da duruyor (analist koda bakınca da görsün). |
| 5 | `tsc --noEmit` + `lint` temiz | ✔ | `pnpm exec next typegen && pnpm exec tsc --noEmit` → çıktı yok; `pnpm lint` → çıktı yok. `pnpm build` iş tanımında istenmediği için koşulmadı. |
## Şartnameden sapmalar
**1 sapma — `src/features/odeme/components/paket-gorunum-olayi.tsx` (şartnamedeki dosya listesinde yok).**
`/paket` görüntülenmesini ölçen bileşen `?kaynak=` değerini bir allowlist'ten süzüyor; listede olmayan her değer `"diger"`e düşüyor (serbest metnin analitiğe sızmaması için, doğru bir koruma). Allowlist bugün `["kilit", "kredi_bitti", "nav"]` idi. Genişletilmeseydi eklediğim 10 kaynağın **onu da** panelde tek bir `"diger"` kovasında toplanacaktı — yani iş görünüşte biter, ölçüm deliği kapanmazdı. Bağımlılık kapanışı gereği aynı commit'e alındı. Dosya P1/P2/P3 paketlerinin kapsamında değil (CEO §3 kontrol edildi), bu yüzden çakışma beklemiyorum.
Başka sapma yok. Yeni olay adı eklenmedi (`OlayAdi` birliğine dokunulmadı), yeni ölçüm yüzeyi eklenmedi, rehber sayfalarına ölçüm eklenmedi.
## Merge sonrası doğrulanması gerekenler
1. **Canlıda bir kez elle dene:** sihirbazı aç → Rybbit'te `sihirbaz_adim{adim:1}` düşüyor mu; 2. adıma geç, geri dön, tekrar ilerle → `adim:1` ikinci kez düşmemeli. (Lokal dev'de Rybbit script'i yüklenmiyor, bu yüzden burada doğrulanamadı.)
2. **Seri kırılması tarihi:** `analitik` ajanı bu değişikliğin **canlıya çıktığı** tarihi sabit dipnot yapsın (CEO §6'daki istek). Merge tarihi değil, deploy tarihi.
3. **`?kaynak=` değerleri panelde göründü mü:** `paket_goruntulendi` olayında `"diger"` kovası bir süre sonra da kalabalıksa allowlist'te unutulmuş bir yüzey var demektir.
4. `/paket` statik bir sayfa; query string eklenmesi ISR/önbellek davranışını değiştirmiyor (`?kaynak=nav` ve `?hata=token` zaten vardı), ama yayın sonrası ilk `/paket?kaynak=fiyat_paket` isteğinde 200 dönüyor mu diye bakılabilir.
## Bilal'den istenen
1. **Dalı incele ve merge et** — `gece/0922-olcum`, 1 commit, 8 dosya, tamamı görünmez ölçüm; ~5 dk.
2. **`paket-gorunum-olayi.tsx` sapmasını onayla** — şartnamede adı geçmiyordu ama onsuz iş işlevsiz kalıyordu; kabul edilmezse o dosyayı geri alıp 10 kaynağın "diger"e düşmesini kabul etmek gerekir; ~2 dk.
3. **Deploy tarihini not et** — `sihirbaz_adim` serisinin kıyaslanamaz hale geldiği gün; ~1 dk.

View File

@@ -0,0 +1,147 @@
# 09 · P2 — Sihirbaz havuzundaki sessiz sızıntı kapatıldı
Dal: `gece/0922-unitur-kacagi` · Şartname: `06-ceo-kararlari.md` §3 P2 · Teşhis: `05-urap.md` §6
Commit düzeni: (1) kod + bu rapor, (2) `data/yokatlas.db` binary'si.
## Özet (5 madde)
1. **Sızıntı kapandı.** "Devlet üniversitesi istiyorum" diyen öğrencinin aday havuzuna giren 27 KKTC/yurt dışı programı artık devlet filtresinde **0**, vakıf filtresinde **27**. Ölçüldü, çıkarım değil.
2. **126 satır düzeldi**, tam şartnamedeki sayı: 100 `unitur` + 26 `il`. Değişen kolon **yalnız bu ikisi**; `programs` tablosunun 45 kolonu tek tek karşılaştırıldı.
3. **2021–2026 puan/sıra/kontenjan/yerleşen kolonlarında değişen satır: 0.** 34 yıl kolonu, 21.493 satır, öncesi/sonrası birebir. `programs_arsiv`, `netler`, `veri_meta` de dokunulmadı.
4. **Kök neden kapandı, tekrarı denetleniyor.** `unitur-onar.ts`'e NULL koşulundan bağımsız **Aşama D** (`KURUM_DUZELT`) eklendi; `veri-kalite.ts`'e iki yeni ZORUNLU kontrol geldi. Kontroller düzeltme öncesi DB'de **KALDI**, sonrasında **GEÇTİ** verdi; ayrıca tek satır bilerek bozulduğunda ikisi de yakaladı.
5. **Bir kapsam genişletmesi var, bilerek:** şartnamedeki "baz ad başına tek `unitur`" kontrolü Kıbrıs Amerikan'ı (100 satırın 42'si) **yakalayamıyor** — o kurumun iki yazımı da yanlış etiketli olduğu için ortada çelişki yok. Bu yüzden ikinci bir 6 satırlık kontrol eklendi (§4). Onaysız bırakılırsa "kök neden kapandı" cümlesi yalan olurdu.
## 1. Ne değişti
| Dosya | Değişiklik |
|---|---|
| `scripts/unitur-onar.ts` | `KURUM_DUZELT` haritası + **Aşama D** (Aşama A'dan önce koşar). `KKTC_ILLERI` dışa aktarıldı (tek kaynak). Başlık yorumu "yalnızca NULL satırlara dokunur" → A/B/C NULL, D koşulsuz. |
| `scripts/veri-kalite.ts` | Kontrol 13 (baz ad başına tek `unitur`) + kontrol 14 (DEVLET etiketi Türkiye ili ile destekli). Dosyanın **en sonuna** eklendi; mevcut satırlar yeniden düzenlenmedi (P3 ile çakışmayı küçültmek için). |
| `data/yokatlas.db` | 126 satır (ayrı commit). |
`KURUM_DUZELT` içeriği ve her satırın kanıtı — kurumun YÖK Atlas'taki **kendi parantezli ikinci yazımı**:
| Ad (DB'de) | `unitur` önce → sonra | `il` |
|---|---|---|
| KIBRIS AMERİKAN ÜNİVERSİTESİ | DEVLET → **KKTC** (36) | değişmedi |
| KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA) | DEVLET → **KKTC** (6) | değişmedi |
| ULUSLARARASI BALKAN ÜNİVERSİTESİ | DEVLET → **YURTDISI KAMU** (34) | SARAYBOSNA - BOSNA - HERSEK → **ÜSKÜP-MAKEDONYA** (26) |
| ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ | DEVLET → **YURTDISI KAMU** (24) | değişmedi (kurum gerçekten Saraybosna'da) |
`ÜSKÜP-MAKEDONYA` yazımı uydurma değil: kurumun kendi adındaki parantezli ek, DB'deki diğer yurt dışı illerin biçimiyle (`BİŞKEK-KIRGIZİSTAN`, `TÜRKİSTAN-KAZAKİSTAN`, `BAKÜ-AZERBAYCAN`) aynı.
`unitur` dağılımı: `DEVLET 12142→12042`, `KKTC 1145→1187`, `YURTDISI KAMU 99→157`. Toplam sabit.
## 2. Fark raporu (öncesi/sonrası)
Yöntem: düzeltme öncesi DB `sqlite3 .backup` ile scratchpad'e alındı, sonra `ATTACH` ile kolon kolon karşılaştırıldı.
```
programs: önce 21493 → sonra 21493 (satır kaybı/ekleme yok)
programs_arsiv: önce 3735 → sonra 3735 (unitur/il değişen satır: 0)
netler: önce 62975 → sonra 62975
veri_meta: önce 37 → sonra 37
programs, 45 kolon tarandı — DEĞİŞEN: unitur=100, il=26
yıl kolonları (34 kolon: sıra/puan/kontenjan/yerleşen + 2025 ek kontenjanları,
2021-2026) değişen satır: 0
```
`pnpm veri-kalite --onceki` kendi bağımsız kontrolüyle aynı sonucu verdi:
```
GEÇTİ önceki DB: satır kaybı — 0
GEÇTİ önceki DB: geçmiş yıllar korunmuş — 20 kolonda dolu değeri değişen/silinen satır: 0
2025 sağlaması (count | Σsira | Σpuan | Σkontenjan | Σyerlesen)
önceki DB : 21493 | 11099796362 | 5433214.11872 | 756453 | 740586
şimdi, önceki DB'deki aynı id'ler : 21493 | 11099796362 | 5433214.11872 | 756453 | 740586
```
## 3. Kabul kriterleri — tek tek
**(1) `pnpm veri-kalite` temiz ve yeni kontrol çelişkiyi gerçekten yakalıyor.** ✅
```
pnpm veri-kalite --yil 2026 --db data/yokatlas.db --onceki <öncesi>
…
GEÇTİ baz ad başına tek unitur — aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor
bilgi baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen) — HOCA AHMET YESEVİ …/YURTDISI KAMU,YURTDISI VAKIF/17 ; KIRGIZİSTAN-TÜRKİYE MANAS …/29
GEÇTİ DEVLET etiketi Türkiye ili ile destekli — DEVLET etiketli her kurumun en az bir programı Türkiye ilinde
Sonuç: 0 ZORUNLU kontrol kaldı, 3 uyarı. (çıkış kodu 0)
```
3 uyarı **düzeltme öncesiyle birebir aynı**, hiçbiri bu işten doğmadı: `il boş` 110 satır (kapsam dışı), `kimlik ATLANDI` (`data/ham` repoda yok), `yetim netler` 2066.
Yakalama iki yoldan kanıtlandı:
- *Düzeltme öncesi DB'de* (gerçek kusur): `KALDI baz ad başına tek unitur — … ULUSLARARASI BALKAN ÜNİVERSİTESİ/DEVLET,YURTDISI KAMU/48 ; ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ/DEVLET,YURTDISI KAMU/38` ve `KALDI DEVLET etiketi Türkiye ili ile destekli — KIBRIS AMERİKAN ÜNİVERSİTESİ/LEFKOŞA/36 ; … (KKTC-LEFKOŞA)/KIBRIS/6 ; ULUSLARARASI BALKAN …/34 ; ULUSLARARASI SARAYBOSNA …/24` → `Sonuç: 2 ZORUNLU kontrol kaldı`, çıkış kodu 1.
- *Bilerek bozma* (düzeltilmiş DB'nin atılabilir kopyasında **tek** satır `DEVLET`'e döndürüldü): ikisi de yakaladı → `Sonuç: 2 ZORUNLU kontrol kaldı`. Kopya silindi; gerçek DB'ye dokunulmadı.
**(2) `db.ts` devlet filtresi 27 programı artık döndürmüyor, vakıf filtresi döndürüyor.** ✅ Ölçüldü.
`src/lib/db.ts:79-82` filtreleri + `EFEKTIF_SIRA = COALESCE(sira2026, sira2025)` (havuza ancak efektif sırası olan satır girer):
```sql
-- devlet: AND unitur = 'DEVLET'
-- vakıf : AND unitur IS NOT NULL AND unitur <> 'DEVLET'
SELECT COUNT(*) FROM programs
WHERE universite IN ('KIBRIS AMERİKAN ÜNİVERSİTESİ',
'KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)',
'ULUSLARARASI BALKAN ÜNİVERSİTESİ',
'ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ')
AND <filtre> AND COALESCE(sira2026, sira2025) IS NOT NULL;
```
| | önce | sonra |
|---|---|---|
| devlet havuzu | **27** | **0** |
| vakıf havuzu | **0** | **27** |
**(3) `/universiteler` hub'ında üç kurum "Devlet" listesinden çıkıyor.** ✅ (kısmen koddan çıkarım — tarayıcı açılmadı)
`getAllUniversiteler` (`src/lib/katalog.ts:268-271`) `MAX(unitur) GROUP BY universite` okur; `universite-listesi.tsx:44-47` `u.unitur === "DEVLET"` olanı devlet listesine, `VAKIF`/`VAKIF MYO` olanı vakfa, kalanı `digerUniler`'e koyar. Aynı SQL DB'de koşturuldu:
```
önce : KIBRIS AMERİKAN …|DEVLET … (KKTC-LEFKOŞA)|DEVLET ULUSLARARASI BALKAN …|DEVLET ULUSLARARASI SARAYBOSNA …|DEVLET
sonra: KIBRIS AMERİKAN …|KKTC … (KKTC-LEFKOŞA)|KKTC ULUSLARARASI BALKAN …|YURTDISI KAMU ULUSLARARASI SARAYBOSNA …|YURTDISI KAMU
Devlet listesindeki kurum sayısı: 170 → 166
```
166 = 170 − **4 yazım** (3 kurum; Kıbrıs Amerikan'ın iki yazımı bugün iki ayrı slug — `kategoriBul` baz ad birleştirmesi kapsam dışı, BACKLOG #10).
Dördü de `KKTC`/`YURTDISI KAMU` olduğu için vakıf listesine **girmiyor**, "Diğer" grubuna düşüyor — hub'da sayı dengesi bozulmuyor.
**(4) 2021–2026 puan/sıra kolonlarında hiçbir değişiklik yok.** ✅ §2.
Ayrıca: `npx next typegen && npx tsc --noEmit` temiz, `npx eslint scripts/unitur-onar.ts scripts/veri-kalite.ts` temiz. Betik **idempotent**: ikinci koşu `unitur 0 satır, il 0 satır`.
## 4. Kapsam genişletmesi ve gerekçesi (şeffaflık)
Şartname `veri-kalite.ts`'e tek kontrol istiyordu: "baz ad başına tek `unitur`" (~8 satır). Uygulandı — ama düzeltme öncesi DB'de koşturunca **Kıbrıs Amerikan'ı hiç görmüyor**: o kurumun her iki yazımı da `DEVLET` etiketli, yani baz ad içinde çelişki yok. Bu kontrol tek başına 100 satırın yalnız 58'ini koruyor; 42'si (öğrenciye sızan 27 programın 12'si) denetimsiz kalıyordu.
Bu yüzden **kontrol 14** eklendi (6 satır): *DEVLET etiketli bir kurumun en az bir programı Türkiye ilinde olmalı.* Türkiye illerinde tire geçmiyor, yurt dışı iller tireli, KKTC illeri `KKTC_ILLERI`'nde; hiç ili olmayan kurumlar (MSB/İçişleri) hariç tutuldu. Düzeltme öncesi DB'de yanlış 4 yazımın **dördünü birden**, sıfır yanlış pozitifle yakalıyor.
Kapsam **dışında bırakılanlara dokunulmadı**: `il` boş 19 satır, burs/dil ekli adlar (B4), `kategoriBul` düzeltmesi.
## 5. Yeni bulgular (bu geceye alınmadı — backlog önerisi)
| # | Bulgu | Sorgu / kanıt | Önem |
|---|---|---|---|
| A | **`programs_arsiv`'de aynı yanlış etiket sürüyor**: 31 satır ULUSLARARASI BALKAN + 30 satır ULUSLARARASI SARAYBOSNA `unitur='DEVLET'`, 24 satırda il Saraybosna. CEO §6'nın veri mühendisine sorduğu soru buydu. | `SELECT universite, unitur, il, COUNT(*) FROM programs_arsiv WHERE universite LIKE '%BALKAN%' OR universite LIKE '%SARAYBOSNA%' GROUP BY 1,2,3` | **Düşük — kullanıcıya yansımıyor.** `src/` tarafında `programs_arsiv`'i okuyan hiçbir SQL yok (tablo yalnızca `src/lib/kapanan-sluglar.ts`'te bir yorumda geçer); `db.ts` havuzu yalnız `programs`'tan besleniyor. Yine de bir sonraki denetimde yeniden "bulgu" olarak çıkacak; `KURUM_DUZELT`'i arşive de uygulamak tek satırlık iş. |
| B | **`data/yokatlas.db-shm` ve `-wal` git'te izleniyor** (32 KB + 0 bayt). Bunlar SQLite'ın geçici dosyaları: DB her açılıp kapandığında oluşup siliniyor, dolayısıyla `git status` kendiliğinden kirleniyor. | `git ls-files data/` | **Düşük.** Uygulamanın `better-sqlite3` ile `readonly` açılışı eski `-shm` yanındayken de sorunsuz çalışıyor (ölçtüm); ama `sqlite3 -readonly` CLI'ı bu durumda "unable to open database file (14)" veriyor — denetim sorgusu koşarken yanıltır. Öneri: `git rm --cached` + `.gitignore`. **Bu commit'e dahil etmedim**: `.gitignore`'da Bilal'in commit'lenmemiş değişikliği var. |
| C | Baz ad çelişkisi kalan 2 kurum: Manas ve Hoca Ahmet Yesevi (`YURTDISI KAMU` ↔ `YURTDISI VAKIF`). | kontrol 13'ün `bilgi` satırı | **Yok.** İkisi de `<> 'DEVLET'` olduğu için devlet/vakıf filtresini bölmüyor; kurum gerçekten ortak. Bilerek ZORUNLU yapılmadı, `bilgi` olarak görünür duruyor. |
## 6. Kullanıcıya yansıyan etki (düzelen)
| Yüzey | Önce | Sonra |
|---|---|---|
| Sihirbaz → "Devlet üniversitesi" | Havuzda 27 KKTC/Bosna/Makedonya programı | 0 |
| Sihirbaz → "Vakıf" | Aynı 27 program eksik | 27'si havuzda |
| `/universiteler` hub'ı | 3 kurum (4 yazım) "Devlet üniversiteleri" listesinde | "Diğer" grubunda |
| `/universite/uluslararasi-balkan-universitesi` | "Tür: Devlet", şehir Saraybosna (Bosna) | Tür: Yurt dışı kamu, şehir Üsküp-Makedonya |
| Tadımlık satırı (`tadimlik-satiri.tsx:37`) | Aynı semantik, aynı sızıntı | Düzeldi (aynı `unitur` alanını okur) |
## Bilal'den istenen
1. **Dalı incele ve merge kararını ver** (`gece/0922-unitur-kacagi`, 2 commit: kod+rapor, veri). Fark raporu §2'de ve veri commit'inin mesajında; geri dönüş için `veri-2025-son` etiketi duruyor. **~10 dk.**
2. **Elindeki ham CSV'de tek `grep`** — `data/ham/` repoda yok: bu üç kurum kaynak dosyada gerçekten `DEVLET` mi yazıyor? Doğrularsa teşhis kapanır, `ingest.ts`'e aynı harita eklenir. **~5 dk.**
3. **Karar:** §5-A (arşiv tablosundaki aynı yanlış etiket) ve §5-B (`-shm`/`-wal` git'ten çıkarılsın mı) backlog'a mı, yoksa bu dala ek commit mi? İkisi de kullanıcıya yansımıyor. **~2 dk.**

View File

@@ -0,0 +1,64 @@
# 10 · P3 — URAP tek sayı: "Tür" kartıyla takas
**Rol:** yazilimci · **Dal:** `gece/0922-urap` · **Tarih:** 23 Eylül 2026
**Commit'ler:** `68cb69e` (veri sabiti), `4932671` (okuma katmanı + kart takası + kaynak satırı + veri-kalite), `<bu rapor>`
## Özet (5 madde)
1. `src/lib/urap-siralama.ts` eklendi: 198 üniversitenin URAP Türkiye 2025-2026 sırası, `/universite/[slug]` slug'ına anahtarlı sabit + `URAP_KAYNAK_URL`, `URAP_DONEM`, `URAP_TOPLAM` + `urapSirasiBul()`. Yeni DB dosyası, göç, cron, çekme betiği **yok**.
2. Üniversite sayfası metrik ızgarasında takas yapıldı: URAP'ta olan üniversitede "Tür" kartının yerini **URAP TÜRKİYE · 18 / 198 · 2025-2026** alıyor; olmayan 31 kurumda **"Tür" yerinde duruyor**. Kart sayısı 4'te kaldı, `lg` ızgara şablonu değişmedi, yeni bileşen icat edilmedi.
3. Tablonun altına **ayrı ikinci kaynak satırı** eklendi; `VeriKaynagi` kullanılmadı ve kopyalanmadı. Satırda dönem, payda, metodoloji uyarısı ve `urapcenter.org`'a gerçek `<a href>` var (`rel="noopener"`, yeni sekme, **nofollow yok**, adres sabitten).
4. `scripts/veri-kalite.ts`'e **dosyanın en sonuna** 5 URAP kontrolü eklendi (mevcut satırlar yeniden düzenlenmedi; `main()` içine yalnız tek satırlık çağrı girdi). Beşi de geçiyor: 198/198 eşleşme, kesintisiz sıra, azalan puan, 31 kurumluk kapsama boşluğu.
5. `pnpm exec next typegen` + `tsc --noEmit` + `pnpm lint` + `pnpm build` temiz; doğrulama üretilmiş statik HTML üzerinden yapıldı (tarayıcı/dev sunucusu açılmadı).
## Değişen dosyalar
| Dosya | Ne |
|---|---|
| `src/lib/urap-siralama.ts` (**yeni**, 242 satır) | 198 satırlık `URAP_SIRA` sabiti + `URAP_DONEM`/`URAP_TOPLAM`/`URAP_KAYNAK_URL` + `urapSirasiBul()`; dosya başındaki yorumda atıf şartı yazılı |
| `src/lib/katalog.ts` (+7) | `UniDetay`'e tek alan: `urap: UrapSira \| null`; `getUniversiteBySlug` dönüşünde `urapSirasiBul(slug)` |
| `src/features/katalog/components/universite-icerik.tsx` (+89/−10) | Kart takası + `UrapKaynakSatiri` bileşeni (aynı dosyada, ızgaranın altındaki kaynak satırı) |
| `scripts/veri-kalite.ts` (+89) | Dosya sonuna `urapKontrolleri()` (5 kontrol) + `main()` sonunda tek satır çağrı + 3 satır import |
Veri kaynağı: `docs/gece-vardiyasi/2026-09-22/05-urap.md` §1-2'de tarif edilen çıkarım (`rankings.byurlandgroup`, 198 satır), veri mühendisinin scratchpad'e bıraktığı `urap-2025.txt`. **Hiçbir sıra, ad ya da puan elle uydurulmadı**; slug'lar üretimden önce DB'deki 322 ham üniversite adından `uniSayfaSlug` ile türetilen kümeyle karşılaştırıldı (198/198 tuttu, 0 karşılıksız).
## Kabul kriterleri — 6/6
| # | Kriter | Durum | Nasıl doğrulandı |
|---|---|---|---|
| 1 | URAP'ta olan sayfada 4 kart, "Tür" yerinde URAP; olmayanda 4 kart, "Tür" duruyor; hiçbir sayfada "—"/boş kutu yok | ✔ | Üretilmiş statik HTML'den metin çıkarımı: `koc-universitesi` → "Program 53 · Fakülte/Birim 7 · **URAP Türkiye 1 / 198 · 2025-2026** · En iyi sıralama 65"; `bogazici-universitesi` → "URAP Türkiye **18 / 198**"; `dogu-akdeniz-universitesi` → "Program 276 · Fakülte/Birim 17 · **Tür Kktc** · En iyi sıralama 9.061" |
| 2 | URAP sayısının göründüğü her sayfada aynı ekranda aktif atıf linki ve dönem var | ✔ | `koc-universitesi.html` içinde: `<a href="https://newtr.urapcenter.org/Rankings/2025-2026/GENEL-SIRALAMASI-2025" target="_blank" rel="noopener" …>`; HTML'de `nofollow` geçmiyor. Dönem hem kartta (`2025-2026`) hem kaynak satırında. Sayı yalnız 1. sayfada çiziliyor, kaynak satırı da yalnız 1. sayfada |
| 3 | `veri-kalite` kontrolleri geçiyor, eşleşmeyen slug sessizce düşmüyor | ✔ | `pnpm veri-kalite --yil 2026 --db data/yokatlas.db` → 5 URAP satırı: "GEÇTİ … 198 slug'ın tamamı DB'de var", "kayıt 198, URAP_TOPLAM 198", "1..198 kesintisiz", "1152.91 → 70.82", "bilgi URAP dışında 31 kurum". Sonuç: 0 ZORUNLU kaldı (3 uyarı **önceden var olan** uyarılar, URAP'la ilgisiz) |
| 4 | Renk yok, rozet yok, `/universiteler` hub'ında URAP'a göre sıralama/filtre yok | ✔ | Kart mevcut `rounded-xl border-slate-200 bg-white` kutusunun içinde, metin `text-slate-900`/`text-slate-500`; `SectionEyebrow`/`Badge` kullanılmadı, yeni renk sınıfı girmedi. `universite-listesi.tsx` ve hub sayfaları **hiç açılmadı** (`git diff --stat`: 4 dosya) |
| 5 | 24'lük listeye, `/sonuc` tablosuna ve LLM prompt'una URAP girmez | ✔ | Dokunulan dosya kümesi bunu dışlıyor; `src/lib/urap-siralama.ts`'i içe aktaran yalnız `katalog.ts`, `universite-icerik.tsx` ve `veri-kalite.ts` |
| 6 | `tsc` + `lint` + `build` temiz | ✔ | `next typegen` ✓, `tsc --noEmit` çıktısız, `pnpm lint` çıktısız, `pnpm build` "Compiled successfully" + `BUILD_ID` üretildi (229 üniversite sayfası prerender). Build günlüğündeki `BetterAuthError`/Cal Sans uyarıları **önceden var**, worktree'de `.env` olmamasından |
## Atıf şartı nerede karşılanıyor
- **Veri dosyasının başında** (`src/lib/urap-siralama.ts` yorumu): izin şartı ve "link konulamayan yüzeyde sayı gösterilmez" kuralı yazılı — sonraki geliştirici sabiti başka bir yüzeye taşırken şartı görür.
- **Ekranda** (`UrapKaynakSatiri`): tablonun altında, `VeriKaynagi`'den ayrı ikinci satır. Kanonik cümle birebir: *"URAP Türkiye Sıralaması 2025-2026 (198 üniversite arasında) — akademik yayın ve araştırma performansını ölçer; eğitim kalitesini ya da iş bulma oranını ölçmez. Kaynak: urapcenter.org."* Adres `URAP_KAYNAK_URL` sabitinden geliyor, elle yazılmadı.
- **OG görselinde URAP yok**: `opengraph-image` dosyalarına dokunulmadı, `uni.urap` oraya hiç ulaşmıyor.
- **Boş durumda** (31 kurum) sayı olmadığı için link de yok; yalnız tek cümlelik açıklama var. Şart "sayının göründüğü yüzey" için; burada sayı görünmüyor.
## Şartnameden sapmalar
1. **Kaynak satırı yalnız 1. sayfada çiziliyor.** Şartname "tablonun altına" diyor; üniversite tabloları sayfalanıyor (`/universite/<slug>/sayfa/2`…) ve metrik ızgarası yalnız 1. sayfada var. URAP sayısı 2. sayfada görünmediği için kaynak satırını da orada çizmedim — aksi hâlde sayfa 2'de bağlamsız bir URAP cümlesi kalırdı. Tersi isteniyorsa tek koşul kaldırılır (`{ilkSayfa ? <UrapKaynakSatiri … /> : null}`).
2. **Boş durum cümlesinin varyant seçimi `unitur` üzerinden.** `KKTC` / `YURTDISI KAMU` / `YURTDISI VAKIF` → "URAP yalnız Türkiye'deki üniversiteleri sıralıyor."; diğerleri → "…2025-2026 listesinde yer almıyor." **P2 ile kesişme:** P2'nin düzelteceği 3 kurum (Kıbrıs Amerikan, Uluslararası Balkan, Uluslararası Saraybosna) bugün `DEVLET` etiketli olduğu için ikinci cümleyi alıyor. İki cümle de doğru, ama P2 merge edildikten sonra bu üç sayfa kendiliğinden doğru varyanta geçer — ek kod gerekmiyor.
3. **5. veri-kalite kontrolü şartnamedekinden farklı.** Veri mühendisinin önerisi "bir önceki döneme göre listeden düşen üniversite" idi; elimizde önceki dönemin sabiti olmadığı için bunu koşturmak mümkün değil. Yerine aynı riski (sessiz düşme) yakalayan bir kontrol yazdım: URAP dışında kalan **150+ programlı bir Türkiye üniversitesi** varsa UYARI — bu neredeyse her zaman ad değişikliği demektir. Bugün 0 (en büyük eksik Türkiye kurumu Mudanya, 78 program).
4. **`veri-kalite.ts`'e 3 satır import eklendi** (dosyanın en sonuna değil, import bloğunun sonuna). Kontrol gövdesi dosyanın en sonunda, `main()` içinde yalnız tek satırlık çağrı var; mevcut satırlar yeniden düzenlenmedi. P2 ile çakışma yüzeyi bu 4 satır.
## Merge sonrası doğrulanması gerekenler
- **P2 (`gece/0922-unitur-kacagi`) merge edildikten sonra:** `dogu-akdeniz-universitesi` dışında, düzeltilen 3 kurumun sayfasında boş durum cümlesinin "URAP yalnız Türkiye'deki üniversiteleri sıralıyor." varyantına geçtiği. Kod değişikliği gerekmez, yalnız gözle bakılır.
- **`scripts/veri-kalite.ts` merge'ü:** P2 aynı dosyaya "baz ad başına tek `unitur`" kontrolü ekliyor. Çakışma beklenen yer import bloğunun sonu; iki taraf da birleştirildikten sonra `pnpm veri-kalite --yil 2026 --db data/yokatlas.db` bir kez koşturulmalı (0 ZORUNLU bekleniyor).
- **Yayın sonrası:** `/universite/bogazici-universitesi` ve `/universite/dogu-akdeniz-universitesi` canlıda açılıp kartın 4'te kaldığı ve kaynak satırındaki linkin URAP sayfasına gittiği bir kez doğrulanmalı.
## Gözlem (kapsam dışı, düzeltilmedi)
"Tür" kartı artık yalnız URAP'ta olmayan 31 kurumda görünüyor ve bunların çoğu KKTC/yurt dışı. `trBaslikDuzeni` bu değerleri **"Kktc"**, **"Yurtdisi Kamu"** diye yazıyor — eskiden de böyleydi ama şimdi bu yazım yalnız o sayfalarda kaldığı için daha görünür. Şartnamede yok, dokunmadım; backlog önerisi.
## Bilal'den istenen
1. **URAP izin e-postasını `docs/` altına koy** (kanıt) — CEO kararı §5-3'te de yazıyor. ~2 dk.
2. **İki sayfayı gözle gör:** `/universite/bogazici-universitesi` (URAP 18/198 kartı + alttaki kaynak satırı) ve `/universite/dogu-akdeniz-universitesi` ("Tür" kartı yerinde + tek cümlelik boş durum). ~3 dk.
3. **Karar:** kaynak satırı derin sayfalarda (`/universite/<slug>/sayfa/2`) da görünsün mü? Bugün görünmüyor, çünkü orada URAP sayısı da yok (sapma #1). ~1 dk.

View File

@@ -3,8 +3,10 @@
*
* YÖK Atlas API'si bazı kayıtlarda universiteTuru döndürmüyor; DB'de ~2.081
* satır unitur=NULL kalıyor ve bunlar hem devlet/vakıf filtrelerinden hem
* sihirbaz sayaçlarından sessizce düşüyor. Onarım üç aşamalı:
* sihirbaz sayaçlarından sessizce düşüyor. Onarım dört aşamalı:
*
* D) Kaynak veride DOLU ama YANLIŞ olan kurum alanları (KURUM_DUZELT) —
* `programs` VE `programs_arsiv` üzerinde
* A) Aynı üniversitenin dolu satırından kopyala (Nişantaşı, Beykent…)
* B) Kalanlarda il KKTC yerleşimiyse unitur='KKTC'
* C) Küçük manuel eşleme (Cerrahpaşa, MSB/İçişleri, Alanya, Manas…)
@@ -13,20 +15,24 @@
* duplikat adlar normalize edilir; refresh.ts insert yolunda da aynı
* normalizasyon uygulanır ki duplikat bir daha oluşmasın.
*
* İdempotenttir; yalnızca NULL satırlara dokunur.
* İdempotenttir. A/B/C yalnızca `programs` tablosunun NULL satırlarına dokunur;
* D (KURUM_DUZELT) NULL koşulundan bağımsızdır — düzelttiği alanlar dolu ama
* yanlış — ve `programs_arsiv`'i de tarar (varsa).
*
* Kullanım: npx tsx scripts/unitur-onar.ts (dev sunucu KAPALIYKEN —
* uygulama DB'yi readonly açar ama WAL checkpoint için tek yazar olmalı)
*/
import path from "node:path";
import Database from "better-sqlite3";
import { tabloVar } from "./yokatlas-goc";
/** Tire etrafı boşluk + çift boşluk temizliği (üniversite adı için). */
export function uniAdiNormalize(ad: string): string {
return ad.replace(/\s*-\s*/g, "-").replace(/\s{2,}/g, " ").trim();
}
const KKTC_ILLERI = [
/** KKTC il değerleri — tek kaynak; veri-kalite.ts de bunu okur. */
export const KKTC_ILLERI = [
"GİRNE",
"LEFKOŞA",
"GAZİMAĞUSA",
@@ -51,6 +57,49 @@ const MANUEL_IL: Record<string, string> = {
"İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "İSTANBUL",
};
/**
* Aşama D — kaynak veride DOLU ama YANLIŞ olan kurum alanları.
*
* MANUEL_UNITUR'dan farkı: burada değer NULL değil, yanlış. Bu yüzden bu
* harita NULL koşulu OLMADAN çalışır. Her satırın kanıtı kurumun YÖK Atlas'ta
* geçen parantezli ikinci yazımıdır (aynı kurum, doğru etiketli):
*
* "ULUSLARARASI BALKAN ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" → YURTDISI KAMU
* "ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ (SARAYBOSNA-…)" → YURTDISI KAMU
* "KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)" → ili KKTC
*
* Yanlış etiket ilk CSV ingest'inden (scripts/ingest.ts) geliyor; refresh.ts
* `uniturSec` DB'deki değeri sadakatle kopyaladığı için her tazelemede
* yeniden üretiliyordu. Düzeltme buraya yazılınca kalıcı olur.
*
* Etki: `src/lib/db.ts` devlet filtresi (`unitur = 'DEVLET'`) bu kurumların
* programlarını sihirbaz havuzuna alıyordu; vakıf filtresi (`unitur <>
* 'DEVLET'`) ise dışarıda bırakıyordu.
*
* `il` düzeltmesi yalnız YANLIŞ yazılmış değeri hedefler (yanlis → dogru);
* boş il'ler kapsam dışı — eksik, eksik olarak görünmeli.
*
* Aynı yanlış etiket `programs_arsiv`'e de yazılmıştı (kapanan programlar oraya
* `programs`'tan olduğu gibi taşınıyor). Bugün `src/` tarafında arşivi okuyan
* SQL yok, ama tablo ileride okunursa sızıntı geri gelirdi; bu yüzden Aşama D
* iki tabloyu da tarar. Dokunulan kolon yalnız `unitur` ve `il`.
*/
const KURUM_DUZELT: Record<
string,
{ unitur?: string; il?: { yanlis: string; dogru: string } }
> = {
"KIBRIS AMERİKAN ÜNİVERSİTESİ": { unitur: "KKTC" },
"KIBRIS AMERİKAN ÜNİVERSİTESİ (KKTC-LEFKOŞA)": { unitur: "KKTC" },
"ULUSLARARASI BALKAN ÜNİVERSİTESİ": {
unitur: "YURTDISI KAMU",
// Kurum Üsküp'te; 26 satırda ili Saraybosna yazıyor. Doğru değerin yazımı
// DB'deki diğer yurt dışı illerin biçimini izler (BİŞKEK-KIRGIZİSTAN…)
// ve kurumun kendi parantezli adından alınmıştır.
il: { yanlis: "SARAYBOSNA - BOSNA - HERSEK", dogru: "ÜSKÜP-MAKEDONYA" },
},
"ULUSLARARASI SARAYBOSNA ÜNİVERSİTESİ": { unitur: "YURTDISI KAMU" },
};
export function uniturOnar(db: Database.Database): void {
// 0) Üniversite adı normalizasyonu (Cerrahpaşa duplikatını birleştirir)
const adlar = db
@@ -69,6 +118,35 @@ export function uniturOnar(db: Database.Database): void {
}
if (duzeltilen > 0) console.log(`Ad normalizasyonu: ${duzeltilen} kurum`);
// D) Dolu ama yanlış kurum alanları. A'dan ÖNCE koşar: yanlış değer başka
// satırlara kopyalanmadan düzelsin. NULL koşulu yok; değer zaten dolu.
// `programs` ve (varsa) `programs_arsiv` aynı haritadan geçer — iki tablo
// ayrışırsa arşiv ileride okunduğunda aynı sızıntı geri gelir.
const dTablolar = tabloVar(db, "programs_arsiv")
? ["programs", "programs_arsiv"]
: ["programs"];
for (const tablo of dTablolar) {
const dUnitur = db.prepare(
`UPDATE ${tablo} SET unitur = ? WHERE universite = ? AND unitur IS NOT ?`
);
const dIl = db.prepare(
`UPDATE ${tablo} SET il = ? WHERE universite = ? AND il = ?`
);
let dUniturToplam = 0;
let dIlToplam = 0;
for (const [ad, duzelt] of Object.entries(KURUM_DUZELT)) {
if (duzelt.unitur) {
dUniturToplam += dUnitur.run(duzelt.unitur, ad, duzelt.unitur).changes;
}
if (duzelt.il) {
dIlToplam += dIl.run(duzelt.il.dogru, ad, duzelt.il.yanlis).changes;
}
}
console.log(
`Aşama D (yanlış kurum alanı) ${tablo}: unitur ${dUniturToplam} satır, il ${dIlToplam} satır`
);
}
// A) Aynı üniversitenin dolu satırından kopyala. Birden fazla FARKLI dolu
// değer varsa sessizce seçim yapma — kaynak veride tutarsızlık demektir.
const celiskiler = db
@@ -148,11 +226,18 @@ if (process.argv[1]?.endsWith("unitur-onar.ts")) {
db.pragma("journal_mode = WAL");
db.transaction(() => uniturOnar(db))();
db.pragma("wal_checkpoint(TRUNCATE)");
for (const tablo of tabloVar(db, "programs_arsiv")
? ["programs", "programs_arsiv"]
: ["programs"]) {
const dagilim = db
.prepare(
"SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM programs GROUP BY unitur ORDER BY n DESC"
`SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM ${tablo} GROUP BY unitur ORDER BY n DESC`
)
.all() as { t: string; n: number }[];
console.log("Dağılım:", dagilim.map((d) => `${d.t}=${d.n}`).join(" "));
console.log(
`Dağılım (${tablo}):`,
dagilim.map((d) => `${d.t}=${d.n}`).join(" ")
);
}
db.close();
}

View File

@@ -21,6 +21,7 @@ import { EFEKTIF_SIRA_SQL, ILK_YIL } from "../src/lib/veri-yillari";
import { EFEKTIF_INDEKS, efektifSorguPlani } from "./db-index";
import { kimlikFarklari, onarimKayitlari, yilKolonlari, type DbKimlik } from "./kimlik";
import { hamOku, sonHamDosya, TUR_MAP } from "./refresh";
import { KKTC_ILLERI } from "./unitur-onar";
import {
argOku,
bayrakVar,
@@ -33,6 +34,10 @@ import {
tabloVar,
yilArgumani,
} from "./yokatlas-goc";
// URAP kontrolleri (dosya sonundaki urapKontrolleri) — salt okunur sabit.
import type { Db } from "./yokatlas-goc";
import { uniSayfaSlug } from "../src/lib/slug";
import { URAP_DONEM, URAP_SIRA, URAP_TOPLAM } from "../src/lib/urap-siralama";
const TURLER = ["SAYISAL", "EŞİT AĞIRLIK", "SÖZEL", "DİL", "TYT"];
@@ -291,9 +296,141 @@ async function main() {
console.log(` şimdi, tüm satırlar (yeni dahil) : ${toplamlar(simdi)}`);
}
// 13. Aynı kurumun iki yazımı arasında unitur çelişkisi — BAZ AD üzerinden.
// unitur-onar.ts'in eski çelişki koruması GROUP BY universite (TAM ad) ile
// çalıştığı için "X ÜNİVERSİTESİ" ve "X ÜNİVERSİTESİ (ÜSKÜP-MAKEDONYA)" iki
// ayrı kurum sayılıyor, aralarındaki DEVLET↔YURTDISI KAMU çelişkisi hiç
// görünmüyordu. Baz ad = sondaki parantezli ek atılmış hâl.
const BAZ_AD =
"TRIM(CASE WHEN instr(universite, '(') > 0 THEN substr(universite, 1, instr(universite, '(') - 1) ELSE universite END)";
// 13 ve 14 arşivi de tarar: kapanan programlar `programs_arsiv`'e olduğu gibi
// taşındığı için yanlış etiket orada da duruyordu (BACKLOG #21). Bugün `src/`
// tarafında arşivi okuyan SQL yok, ama iki tablo ayrışırsa denetim bunu
// görmez ve tablo ileride okunduğunda sızıntı geri gelir.
const KURUMLAR = tabloVar(db, "programs_arsiv")
? `(SELECT universite, unitur, il FROM programs
UNION ALL SELECT universite, unitur, il FROM programs_arsiv)`
: "programs";
const bazAdCeliskileri = `(SELECT ${BAZ_AD} AS bazAd, GROUP_CONCAT(DISTINCT unitur) AS turler, COUNT(*) AS adet,
MAX(unitur = 'DEVLET') <> MIN(unitur = 'DEVLET') AS devletBolunmus
FROM ${KURUMLAR} WHERE unitur IS NOT NULL GROUP BY 1 HAVING COUNT(DISTINCT unitur) > 1)`;
const bolunmus = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE devletBolunmus`);
zorunlu(
"baz ad başına tek unitur",
bolunmus === "",
bolunmus === ""
? "aynı kurumun iki yazımı devlet/vakıf filtresini bölmüyor"
: `devlet filtresine giren, vakıf filtresinden düşen kurum: ${bolunmus} → scripts/unitur-onar.ts KURUM_DUZELT`
);
const esdegerFark = ornek(`SELECT bazAd, turler, adet FROM ${bazAdCeliskileri} WHERE NOT devletBolunmus`);
bilgi("baz ad unitur farkı (devlet/vakıf ayrımını bölmeyen)", false, esdegerFark || "yok");
// 14. DEVLET etiketli ama hiçbir programı Türkiye ilinde olmayan kurum.
// 13'ün kör noktası: kurumun İKİ yazımı da yanlış etiketliyse çelişki yoktur.
// Türkiye illerinde tire geçmez; yurt dışı iller tireli (BİŞKEK-KIRGIZİSTAN…),
// KKTC illeri KKTC_ILLERI'nde. İli hiç olmayan kurumlar (MSB/İçişleri) hariç.
const yurtDisiDevlet = ornek(
`SELECT universite, GROUP_CONCAT(DISTINCT il), COUNT(*) FROM ${KURUMLAR}
WHERE unitur = 'DEVLET' GROUP BY universite
HAVING SUM(il IS NOT NULL) > 0
AND SUM(il NOT LIKE '%-%' AND il NOT IN (${KKTC_ILLERI.map(() => "?").join(",")})) = 0`,
...KKTC_ILLERI
);
zorunlu(
"DEVLET etiketi Türkiye ili ile destekli",
yurtDisiDevlet === "",
yurtDisiDevlet === ""
? "DEVLET etiketli her kurumun en az bir programı Türkiye ilinde"
: `tüm programları KKTC/yurt dışında olduğu hâlde DEVLET: ${yurtDisiDevlet}`
);
urapKontrolleri(db, zorunlu, bilgi);
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);
/**
* URAP Türkiye sıralaması sabiti (`src/lib/urap-siralama.ts`) denetimi.
* Amaç: yıllık tazelemede eşleşmeyen bir URAP satırı SESSİZCE DÜŞMESİN —
* düşerse üniversitenin adı değişmiş demektir ve o sayfada sıra kaybolur.
* DB'ye yazmaz; sabit ile DB'deki üniversite slug'larını karşılaştırır.
*/
function urapKontrolleri(
db: Db,
zorunlu: (ad: string, tamam: boolean, detay: string) => void,
bilgi: (ad: string, sorunlu: boolean, detay: string) => void
) {
const kayitlar = Object.entries(URAP_SIRA);
// (1) Sabitteki her slug DB'de karşılık bulmalı — bulamayan = ad değişikliği.
const dbToplam = new Map<string, { unitur: string | null; program: number }>();
const satirlar = db
.prepare("SELECT universite, unitur, COUNT(*) AS n FROM programs GROUP BY 1, 2")
.all() as { universite: string; unitur: string | null; n: number }[];
for (const r of satirlar) {
const slug = uniSayfaSlug(r.universite);
if (!slug) continue;
const kayit = dbToplam.get(slug) ?? { unitur: null, program: 0 };
kayit.program += r.n;
if (!kayit.unitur && r.unitur) kayit.unitur = r.unitur;
dbToplam.set(slug, kayit);
}
const karsiliksiz = kayitlar.filter(([slug]) => !dbToplam.has(slug));
zorunlu(
"URAP: her sıra bir üniversite sayfasına bağlı",
karsiliksiz.length === 0,
karsiliksiz.length === 0
? `${kayitlar.length} slug'ın tamamı DB'de var`
: `DB'de karşılığı olmayan (ad değişmiş olabilir): ${karsiliksiz.map(([slug, v]) => `${slug}(#${v.sira})`).join(", ")}`
);
// (2) Kayıt sayısı payda ile aynı olmalı — payda kartta gösteriliyor.
zorunlu(
"URAP: kayıt sayısı = URAP_TOPLAM",
kayitlar.length === URAP_TOPLAM,
`kayıt ${kayitlar.length}, URAP_TOPLAM ${URAP_TOPLAM} (dönem ${URAP_DONEM})`
);
// (3) Sıralar 1..URAP_TOPLAM; tekrarsız ve boşluksuz.
const siralar = kayitlar.map(([, v]) => v.sira).sort((a, b) => a - b);
const bozukSira = siralar.findIndex((s, i) => s !== i + 1);
zorunlu(
"URAP: sıralar 1..N tekrarsız ve boşluksuz",
bozukSira === -1 && siralar.length === URAP_TOPLAM,
bozukSira === -1
? `1..${siralar.length} kesintisiz`
: `${bozukSira + 1}. sırada beklenen ${bozukSira + 1}, gelen ${siralar[bozukSira]}`
);
// (4) Puan sıraya göre azalmalı — sıra 1 en yüksek puan.
const sirali = [...kayitlar].sort((a, b) => a[1].sira - b[1].sira);
const bozukPuan = sirali.findIndex(
(k, i) => i > 0 && sirali[i - 1][1].puan < k[1].puan
);
zorunlu(
"URAP: puanlar sıraya göre azalan",
bozukPuan === -1,
bozukPuan === -1
? `${sirali[0][1].puan} → ${sirali[sirali.length - 1][1].puan}`
: `#${sirali[bozukPuan][1].sira} (${sirali[bozukPuan][0]}) puanı bir önceki sıradan yüksek`
);
// (5) Kapsama boşluğu: URAP yalnız Türkiye'yi sıralar, KKTC/yurt dışı ve
// yalnız MYO'su olan kurumlar listede olmaz — bu normaldir. Ama büyük bir
// Türkiye üniversitesinin düşmesi neredeyse her zaman ad değişikliğidir.
const disarida = [...dbToplam.entries()].filter(([slug]) => !(slug in URAP_SIRA));
const turkiyeBuyuk = disarida.filter(
([, v]) =>
(v.unitur === "DEVLET" || v.unitur === "VAKIF") && v.program >= 150
);
bilgi(
"URAP: kapsama boşluğu",
turkiyeBuyuk.length > 0,
turkiyeBuyuk.length === 0
? `URAP dışında ${disarida.length} kurum (KKTC/yurt dışı, MYO ve yeni üniversiteler); 150+ programlı Türkiye üniversitesi yok`
: `URAP dışında kalan 150+ programlı Türkiye üniversitesi (ad değişmiş olabilir): ${turkiyeBuyuk.map(([slug, v]) => `${slug}(${v.program})`).join(", ")}`
);
}

View File

@@ -52,7 +52,7 @@ export default function KosullarPage() {
Temel program arama ücretsizdir. Tercih Dönemi Paketi tek seferlik
bir satın almadır; abonelik değildir, otomatik yenilenmez. Paket
içeriği ve güncel fiyat{" "}
<Link href="/paket" className="text-primary underline">
<Link href="/paket?kaynak=kosullar" className="text-primary underline">
paket sayfasında
</Link>{" "}
yazar.

View File

@@ -540,7 +540,7 @@ export default function Home() {
asChild
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href="/paket">
<Link href="/paket?kaynak=fiyat_paket">
{PAKET_FIYATI} TL — Paketi incele
<ArrowRight className="size-4" aria-hidden />
</Link>
@@ -570,7 +570,7 @@ export default function Home() {
variant="outline"
className="mt-4 h-11 w-full cursor-pointer"
>
<Link href="/paket">
<Link href="/paket?kaynak=fiyat_kredi">
+{URUNLER.topup.credits} kredi — {TOPUP_FIYATI} TL
<ArrowRight className="size-4" aria-hidden />
</Link>

View File

@@ -11,6 +11,7 @@ import {
SonucProgramTablosuSkeleton,
} from "@/features/liste/components/sonuc-program-tablosu";
import { ManuelHarita } from "@/features/liste/components/manuel-harita";
import { LISTE_SAYI_CEREZ_ADI } from "@/features/liste/liste-dogrula";
import { TadimlikBolumu } from "@/features/rapor/components/tadimlik-bolumu";
import { SonucSihirbazi } from "@/features/sihirbaz/components/sihirbaz-bolumu";
import { SiraGerekli } from "@/features/sihirbaz/components/sira-gerekli";
@@ -85,6 +86,17 @@ export default function SonucPage({
const uniturGrubu =
tipHam === "devlet" || tipHam === "vakif" ? tipHam : undefined;
// Manuel liste localStorage'ta yaşar; store'un sunucu snapshot'ı her
// zaman boştur. Client, listenin yalnızca UZUNLUĞUNU çereze aynalar
// (liste-store.ts) — sunucunun "bu ziyaretçinin listesi var" bilgisini
// hidrasyondan ÖNCE alabildiği tek kanal bu. Sayı > 0 ise haritaya tam
// ölçülü yer ayrılır; çerez yoksa/0 ise harita hiç render edilmez.
const listeSayisi = Number.parseInt(
cerezler.get(LISTE_SAYI_CEREZ_ADI)?.value ?? "",
10,
);
const haritayaYerAyir = Number.isFinite(listeSayisi) && listeSayisi > 0;
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
return (
<>
@@ -97,21 +109,26 @@ export default function SonucPage({
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Sayfa hiyerarşisi: tadımlık → ham tablo → manuel listenin
haritası. Tadımlık, ödeme öncesi gösterebildiğimiz TEK gerçek
önizleme olduğu için ilk ekranda durur.
{/* Sayfa hiyerarşisi: manuel listenin haritası → tadımlık → ham
tablo. İki farklı ziyaretçi, iki farklı ilk ekran:
Harita neden TABLONUN ALTINDA: liste store'unun sunucu
snapshot'ı her zaman boş liste döner (liste-store.ts), kayıtlı
liste ancak hidrasyondan sonra okunur. Harita yukarıda olsaydı
dönüş ziyaretçisinde sayfa açıldıktan saniyeler sonra ~368 px
yüksekliğinde belirip tadımlığı ve tabloyu aşağı itecekti
(impact fraction ~1, CLS eşiğinin çok üstü). Aşağıda belirince
yalnızca sihirbaz ile dipnotu iter, ikisi de katlanın altında.
Bilgi mimarisi olarak da doğru sıra: harita, tablodan kurulan
listenin görselleştirmesidir. Liste boşken hiç render edilmez
(bkz. manuel-harita.tsx); store'a abone olduğu için tablodan
"+" ile eklenenler buraya canlı yansır. */}
• Listesi OLAN ziyaretçi ekranda önce kendi kurduğu listeyi
görür — o kişi "bu ürün ne?" aşamasını geçmiştir.
• Listesi OLMAYAN ziyaretçide harita DOM'a hiç girmez
(manuel-harita.tsx), sıra bugünküyle birebir aynı kalır:
tadımlık → tablo. Tadımlık, ödeme öncesi gösterebildiğimiz
TEK gerçek önizleme olduğu için o kişide ilk ekranda durur
ve `first:mt-0` ile main'in py-12'sini kullanmayı sürdürür.
Harita YUKARIDA nasıl CLS üretmiyor: kayıtlı liste ancak
hidrasyondan sonra localStorage'tan okunabildiği için harita
yukarıdayken sonradan belirip sayfayı iterdi (mobilde ~146 px,
masaüstünde ~368 px). Çözüm, listenin SAYISINI çerezle
sunucuya aynalamak (haritayaYerAyir, yukarısı): sunucu ilk
boyada haritanın tam ölçülü kutusunu çizer, chunk inince
kutunun yalnızca İÇİ dolar. Haritanın yerini bir daha
aşağı almadan önce bu mekanizmayı kontrol et. */}
<ManuelHarita yerAyir={haritayaYerAyir} />
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
Havuzda içerik yoksa bileşen hiç render edilmez. */}
@@ -128,8 +145,6 @@ export default function SonucPage({
uniturGrubu={uniturGrubu}
/>
<ManuelHarita />
{/* Sihirbaz akışı (CTA + modal) — üretim ve liste /listem'de
yaşar, burada yalnızca funnel kartları render edilir */}
<SonucSihirbazi

View File

@@ -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 { UNITUR_ETIKET } from "@/lib/unitur";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
import {
@@ -224,7 +225,8 @@ export function BolumProgramListesi({
: "bg-violet-50 text-violet-600"
}`}
>
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
{UNITUR_ETIKET[p.unitur]?.cip ??
trBaslikDuzeni(p.unitur)}
</span>
) : null}
<span className="truncate">

View File

@@ -9,6 +9,7 @@ import {
bolumSlugFromIsim,
} from "../katalog-queries";
import { trBaslikDuzeni } from "@/lib/slug";
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
import {
JsonLd,
breadcrumbJsonLd,
@@ -30,6 +31,11 @@ import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
import { VeriKaynagi } from "./veri-kaynagi";
import {
URAP_DONEM,
URAP_KAYNAK_URL,
URAP_TOPLAM,
} from "@/lib/urap-siralama";
import { UniLogo } from "@/components/uni-logo";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { Skeleton } from "@/components/ui/skeleton";
@@ -109,6 +115,48 @@ function fakulteOzeti({
return `${adlar[0]}, ${adlar[1]} ve ${adlar.length - 2} birim daha`;
}
/**
* Tablonun altındaki AYRI ikinci kaynak satırı. `VeriKaynagi` kullanılmaz ve
* kopyalanmaz: o satır YÖK Atlas'ın yerleştirme yılına ve son güncelleme
* tarihine bağlı, URAP'ın dönemi başka — aynı satırda birleşirlerse URAP
* verisi YÖK Atlas tarihiyle sunulmuş olur.
*
* Atıf pazarlık dışı: URAP'ın izni "urapcenter.org adresine aktif bağlantı"
* şartına bağlı, o yüzden gerçek <a href>, yeni sekme, rel="noopener" ve
* nofollow YOK. Sıra payda ("198 üniversite arasında") ve dönem olmadan
* gösterilmez. URAP listesinde olmayan üniversitede sayı yoktur; eksiklik
* gizlenmez, nedeni tek cümleyle yazılır.
*/
function UrapKaynakSatiri({ uni }: { uni: UniDetay }) {
if (!uni.urap) {
const yurtDisi =
uni.unitur === "KKTC" || uni.unitur?.startsWith("YURTDISI");
return (
<p className="mt-2 text-xs leading-5 text-slate-500">
{yurtDisi
? "Bu üniversite URAP Türkiye sıralamasında yer almıyor — URAP yalnız Türkiye'deki üniversiteleri sıralıyor."
: `Bu üniversite URAP Türkiye Sıralaması ${URAP_DONEM} listesinde yer almıyor.`}
</p>
);
}
return (
<p className="mt-2 text-xs leading-5 text-slate-500">
URAP Türkiye Sıralaması {URAP_DONEM} ({sayi(URAP_TOPLAM)} üniversite
arasında) — akademik yayın ve araştırma performansını ölçer; eğitim
kalitesini ya da iş bulma oranını ölçmez. Kaynak:{" "}
<a
href={URAP_KAYNAK_URL}
target="_blank"
rel="noopener"
className="font-medium text-primary hover:underline"
>
urapcenter.org
</a>
.
</p>
);
}
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
const uni = getUniversiteBySlug(slug);
if (!uni) return {};
@@ -165,6 +213,13 @@ export function UniversiteIcerik({
}
const ilkSayfa = sayfa === 1;
// Tür iki yerde geçer ve bu bilinçli: H1'in altındaki çip taranmak,
// özet paragrafın ilk virgülündeki öbek okunmak içindir. Izgarada "Tür"
// kartı yok (şablon 4 hücre, bkz. aşağıdaki ızgara yorumu). Haritada
// olmayan ya da NULL bir `unitur`'da öbek hiç yazılmaz: cümlede kopuk
// virgül/boşluk kalmaz ve ham DB değeri cümlenin içine sızmaz.
const turEtiketi = uni.unitur ? UNITUR_ETIKET[uni.unitur] : undefined;
const turCumlesi = turEtiketi?.cumle;
const buYol = ilkSayfa
? `/universite/${slug}`
: `/universite/${slug}/sayfa/${sayfa}`;
@@ -258,12 +313,38 @@ export function UniversiteIcerik({
</h1>
</div>
{/* Kurumun kimliği (tür + il): ızgaradaki kartlar sayısal ve
karşılaştırılabilir metriklerdir, tür ise kategoridir — sitenin
geri kalanında da başlığın altında çip olarak duruyor. Çipin sınıf
seti program-tablosu.tsx:329-333'ten birebir kopyadır (AGENTS.md
"ortak elemanlar tek kaynaktan"); yeni varyant üretilmez.
Sayfalama sayfalarında da yazar: ziyaretçi Google'dan doğrudan
/sayfa/N'ye inebiliyor, kimlik orada da aynı. `unitur` NULL ise
satırın tamamı düşer, kopuk boşluk kalmaz. */}
{turEtiketi ? (
<p className="mt-3 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
<span
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
uniturDevletMi(uni.unitur)
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{turEtiketi.cip}
</span>
{uni.il ? (
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
) : null}
</p>
) : null}
{ilkSayfa ? (
<>
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
{uni.ad}
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}, {SON_YIL} YKS
yerleştirme sonuçlarına göre{" "}
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}
{turCumlesi ? `, ${turCumlesi};` : ","} {SON_YIL} YKS yerleştirme
sonuçlarına göre{" "}
<strong>{sayi(uni.programSayisi)} programda</strong> öğrenci aldı
{uni.fakulteSayisi > 1
? `; programlar ${sayi(uni.fakulteSayisi)} fakülte/yüksekokula dağılıyor`
@@ -308,17 +389,38 @@ export function UniversiteIcerik({
{sayi(uni.fakulteSayisi)}
</dd>
</div>
{/* Şablon tam 4 hücre bırakıyor: masaüstünde harita 3. sütunu
iki satır boyunca kaplıyor (lg:grid-rows-2 + row-span-2), 5.
kart ızgarayı bozar. Bu yüzden 4. hücre tek: URAP'ta olan
üniversitede URAP sırası, olmayan 31 sayfada toplam
kontenjan. Toplam kontenjanı 0 olan üniversite yok
(yokatlas.db: GROUP BY universite HAVING
SUM(COALESCE(kontenjan2026,0))=0 → 0 satır), yani hiçbir
sayfada "—" çıkmaz. URAP'ta payda kartın içindedir: "18."
tek başına yanıltır. */}
<div className="rounded-xl border border-slate-200 bg-white p-4">
{uni.urap ? (
<>
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
Tür
URAP Türkiye
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{uni.unitur === "DEVLET"
? "Devlet"
: uni.unitur
? trBaslikDuzeni(uni.unitur)
: "—"}
{sayi(uni.urap.sira)} / {sayi(URAP_TOPLAM)}
<span className="mt-0.5 block font-sans text-xs font-medium text-slate-500">
{URAP_DONEM}
</span>
</dd>
</>
) : (
<>
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
Toplam kontenjan
</dt>
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
{sayi(uni.stats.toplamKontenjan)}
</dd>
</>
)}
</div>
<div className="rounded-xl border border-slate-200 bg-white p-4">
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
@@ -371,6 +473,8 @@ export function UniversiteIcerik({
veya o yıl yerleşen çıkmayan program).{" "}
</VeriKaynagi>
{ilkSayfa ? <UrapKaynakSatiri uni={uni} /> : null}
<Sayfalama
tabanYol={`/universite/${slug}`}
sayfa={sayfa}

View File

@@ -11,7 +11,7 @@ import type { HaritaPin } from "@/features/liste/components/tercih-haritasi";
import { ilKonum, uniAdiNormalize, uniKonum } from "@/lib/harita";
import { yurtdisiPinKaydet, type YurtdisiPin } from "@/lib/harita-pinler";
import type { RiskSeviyesi } from "@/lib/risk";
import { useManuelListe } from "../liste-store";
import { useListeHidreMi, useManuelListe } from "../liste-store";
// Aynı üniversitede birden çok tercih varsa pinde kötü risk baskın olsun
const RISK_AGIRLIK: Record<RiskSeviyesi, number> = {
@@ -20,8 +20,9 @@ const RISK_AGIRLIK: Record<RiskSeviyesi, number> = {
riskli: 2,
};
export function ManuelHarita() {
export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) {
const liste = useManuelListe();
const hidre = useListeHidreMi();
const [seciliIl, setSeciliIl] = useState<string | null>(null);
const { pinler, yurtdisiPinler, haritaDisi } = useMemo(() => {
@@ -64,18 +65,30 @@ export function ManuelHarita() {
};
}, [liste]);
// Liste boşken hiç yer tutulmaz: boş harita /sonuc'un ilk ekranını işgal
// ediyordu, oysa ödeme öncesi ilk ekranda durması gereken şey tadımlık.
// İlk "+" ile liste dolunca harita kendiliğinden belirir; 197 KB'lık il
// geometrisi yine ancak o anda iner (TercihHaritasiLazy zaten `dynamic`).
if (liste.length === 0) return null;
// Liste boşken hiç yer tutulmaz: hiç liste kurmamış ziyaretçide harita
// DOM'a girmez, yer tutucu da çizilmez (Bilal'in açık şartı). İlk "+" ile
// liste dolunca harita kendiliğinden belirir.
//
// yerAyir: sunucu, kullanıcının kayıtlı bir listesi OLDUĞUNU biliyorsa
// (LISTE_SAYI_CEREZ_ADI çerezi > 0) bu bayrağı verir. Liste ancak
// hidrasyondan sonra localStorage'tan okunduğu için ilk boyada `liste` hâlâ
// boştur; bayrak olmadan harita sonradan belirip sayfayı iterdi. Boş+yerAyir
// durumunda AYRI bir iskelet dalı yazmıyoruz: fonksiyon akar, `pinler` boş
// dizi olur ve TercihHaritasiLazy chunk inene kadar zaten HaritaIskeleti'ni
// gösterir — böylece yer tutucu ile gerçek harita aynı kutudan gelir
// (yükseklik ayrışamaz) ve 197 KB'lık chunk hidrasyonu beklemeden inmeye
// başlar.
//
// yerAyir yalnızca hidrasyona KADAR geçerlidir: store okunduktan sonra tek
// doğru kaynak listenin kendisidir. Böylece hem bayat çerez (localStorage
// silinmiş, çerez kalmış) hem de kullanıcının çekmeceden listeyi boşaltması
// durumunda boş harita ekranda kalmaz.
if (liste.length === 0 && (hidre || !yerAyir)) return null;
// mt-16: /sonuc'ta harita artık tablonun ALTINDA duruyor (bkz. sonuc/page.tsx)
// ve sayfanın bölüm ritmini paylaşıyor — tablo <section>'ı ve sihirbaz
// kartları da mt-16 kullanır. Eskiden main'in ilk çocuğu olduğu için üst
// boşluğu main'in py-12'sinden alıyordu, kendi margin'i yoktu.
// Margin yok: harita /sonuc'ta main'in İLK çocuğudur, üst boşluğunu
// main'in py-12'sinden alır (bkz. sonuc/page.tsx).
return (
<div className="mt-16">
<div>
<TercihHaritasiLazy
pinler={pinler}
seciliIl={seciliIl}

View File

@@ -20,7 +20,12 @@ import type {
PuanTuruKey,
RankResultsListe,
} from "@/types/yokatlas";
import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "@/lib/risk";
import {
DILIM_ETIKET,
RISK_ETIKET,
riskHesapla,
type RiskSeviyesi,
} from "@/lib/risk";
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
@@ -73,6 +78,9 @@ const DILIMLER: {
},
];
// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli. Nokta
// hiçbir yerde tek başına durmaz; yanında RISK_ETIKET'ten gelen ad her ekran
// genişliğinde yazılı durur (renk tek başına anlam taşımaz).
const RISK_NOKTA: Record<RiskSeviyesi, string> = {
guvenli: "bg-emerald-500",
"az-riskli": "bg-amber-500",
@@ -237,7 +245,7 @@ export function ProgramTablosu({
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8" />
<TableHead className="w-8 pl-4 pr-0">Risk</TableHead>
<TableHead>Program</TableHead>
<TableHead className="hidden text-right md:table-cell">
Son {VERI_YIL_SAYISI} yıl
@@ -304,12 +312,15 @@ function ProgramSatiri({
return (
<TableRow className="kt-cv-satir">
<TableCell className="pl-4 pr-0">
<TableCell className="pl-4 pr-0 text-xs font-medium">
{risk ? (
<span className="inline-flex items-center gap-1.5">
<span
className={`block size-2.5 rounded-full ${RISK_NOKTA[risk]}`}
className={`size-2.5 shrink-0 rounded-full ${RISK_NOKTA[risk]}`}
aria-hidden
/>
{RISK_ETIKET[risk]}
</span>
) : null}
</TableCell>
<TableCell className="max-w-0 w-full">

View File

@@ -4,6 +4,10 @@
// çizileceği zaman insin: dynamic + ssr'siz. İskelet, haritanın gerçek
// oranını (viewBox 1050 × 447, bkz. tercih-haritasi TAM_GORUNUM) rezerve
// eder — chunk gelince yerleşim zıplamaz.
//
// motion-reduce:animate-none: iskelet artık /sonuc'ta katlamanın üstünde
// duruyor; hareketi azaltılmış isteyen kullanıcıya sürekli, durdurulamaz
// nabız gösterilmez.
import dynamic from "next/dynamic";
@@ -11,7 +15,7 @@ export function HaritaIskeleti() {
return (
<div
aria-hidden
className="aspect-[1050/447] w-full animate-pulse rounded-xl bg-slate-100"
className="aspect-[1050/447] w-full animate-pulse rounded-xl bg-slate-100 motion-reduce:animate-none"
/>
);
}

View File

@@ -5,7 +5,14 @@
import dynamic from "next/dynamic";
// loading: chunk inene kadar şeridin yüksekliğini rezerve eder. Türkiye
// haritası /sonuc'un en üstüne çıktığı için bu şerit de katlamanın üstünde;
// yer tutucusuz hâlinde yoktan belirip altındaki her şeyi iterdi.
// h-26 (104 px) tek satırlık gerçek yüksekliktir: figür w-52 (208 px),
// svg oranı 1050×447 → 88,6 px, figcaption text-xs → 16 px. Kap mt-3'ü
// bileşenin kendi kökündekiyle aynı (yurtdisi-haritalar.tsx). Birden çok
// ülke alt satıra kayarsa fark kalır — sunucu ülke sayısını bilemez.
export const YurtdisiHaritalarLazy = dynamic(
() => import("./yurtdisi-haritalar").then((m) => m.YurtdisiHaritalar),
{ ssr: false },
{ ssr: false, loading: () => <div className="mt-3 h-26" aria-hidden /> },
);

View File

@@ -6,6 +6,16 @@ import type { RiskSeviyesi } from "@/lib/risk";
export const MANUEL_LISTE_MAX = 24;
/**
* Listenin sunucu aynası — yalnızca ELEMAN SAYISI taşır, hiçbir kişisel veri
* ya da program kimliği taşımaz. Liste localStorage'ta yaşadığı için sunucu
* snapshot'ı her zaman boştur; /sonuc bu çerezi okuyup haritaya hidrasyondan
* önce tam ölçülü yer ayırabilsin diye var (bkz. sonuc/page.tsx).
* Sabit burada durur çünkü bu dosya client store ile sunucu tarafının ortak
* zemini; store'un kendisi "use client".
*/
export const LISTE_SAYI_CEREZ_ADI = "kolaytercih.liste-sayi";
export type ManuelKaynak = "sonuc" | "bolum" | "universite";
export type ManuelTercih = {

View File

@@ -14,6 +14,7 @@ import {
} from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
import {
LISTE_SAYI_CEREZ_ADI,
MANUEL_LISTE_MAX,
dogrula,
type ManuelKaynak,
@@ -39,10 +40,23 @@ function yayinla() {
for (const cb of dinleyiciler) cb();
}
/**
* Sunucu aynası: listenin yalnızca UZUNLUĞU çereze yazılır (kişisel veri yok).
* /sonuc bunu okuyup haritaya hidrasyondan önce tam ölçülü yer ayırır; yoksa
* kayıtlı listesi olan ziyaretçide harita sonradan belirip sayfayı iterdi.
* Öznitelikler profilCerezineYaz ile birebir aynı (sihirbaz-profil.ts).
*/
function sayiCereziniYaz(n: number) {
if (typeof document === "undefined") return;
const guvenli = window.location.protocol === "https:" ? "; secure" : "";
document.cookie = `${LISTE_SAYI_CEREZ_ADI}=${n}; path=/; max-age=31536000; samesite=lax${guvenli}`;
}
function kaydet() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(liste));
} catch {}
sayiCereziniYaz(liste.length);
}
function yukle() {
@@ -52,6 +66,10 @@ function yukle() {
const ham = localStorage.getItem(STORAGE_KEY);
if (ham) liste = dogrula(JSON.parse(ham));
} catch {}
// Çerezi her açılışta gerçek uzunlukla tazele: yeni dağıtımdan sonra ilk
// ziyarette çerezi doğurur, localStorage silinmiş ama çerez kalmış durumda
// da 0 yazıp /sonuc'un boşuna yer ayırmasını engeller.
sayiCereziniYaz(liste.length);
// Profilsiz + ile eklenenlerin riski null'dır. Profil sonradan hangi yoldan
// yazılırsa yazılsın (kapı, hero/CTA formu, sihirbaz, giriş senkronu) riskler
// dolsun: açılışta bir kez, sonra her profil değişiminde.
@@ -62,6 +80,7 @@ function yukle() {
if (e.key !== STORAGE_KEY) return;
try {
liste = e.newValue ? dogrula(JSON.parse(e.newValue)) : [];
sayiCereziniYaz(liste.length);
yayinla();
} catch {}
});
@@ -87,6 +106,20 @@ export function useManuelListe(): ManuelTercih[] {
);
}
/**
* Store'un client'ta hidre olup olmadığı: sunucu snapshot'ı false, hidrasyondan
* sonra true. `liste` ile aynı anda flip eder (ikisi de abone içindeki
* yukle()'den doğar), bu yüzden "boş liste" ile "henüz okunmadı" ayrımı
* gereken yerlerde tek render'da tutarlıdır — bkz. ManuelHarita.
*/
export function useListeHidreMi(): boolean {
return useSyncExternalStore(
abone,
() => yuklendi,
() => false,
);
}
export function useCekmeceAcik(): boolean {
return useSyncExternalStore(
abone,

View File

@@ -42,7 +42,7 @@ export function OdemeUstSerit() {
</span>
</div>
<Link
href="/paket"
href="/paket?kaynak=odeme_geri"
className="inline-flex shrink-0 items-center gap-1.5 text-sm text-slate-500 transition-colors hover:text-slate-700"
>
<ArrowLeft className="size-4" aria-hidden />

View File

@@ -138,7 +138,7 @@ export async function OdemeSonucKarti({ siparis }: { siparis?: string }) {
</p>
<PagePixelDivider seed={83} className="mx-auto mt-5" />
<Button asChild variant="outline" className="mt-6 h-11 cursor-pointer">
<Link href="/paket">Tekrar dene</Link>
<Link href="/paket?kaynak=odeme_hata">Tekrar dene</Link>
</Button>
</div>
);

View File

@@ -11,7 +11,38 @@ import { useEffect } from "react";
import { olayBirKez } from "@/lib/analitik";
// Serbest metin analitiğe sızmasın: bilinmeyen değerler "diger"e düşer.
const KAYNAKLAR = ["kilit", "kredi_bitti", "nav"] as const;
// Yeni bir /paket linki eklerken değerini önce buraya yaz, sonra href'e koy —
// listede olmayan her kaynak "diger"e düşer ve yüzey görünmez kalır.
//
// SERİ KIRILMASI: liste 3'ten 13 değere çıktığı için tekilleştirme anahtarı
// da (aşağıda `kaynak`) çeşitlendi; `paket_goruntulendi`nin oturum başına
// atılabilme tavanı 4'ten 14'e çıktı ve "diger" kovası artık yalnız gerçekten
// bilinmeyen yüzeyleri taşıyor (eskiden 10 bilinen yüzey de oradaydı).
// Bu tarihten önceki/sonraki toplamlar ve "diger" payı karşılaştırılamaz.
const KAYNAKLAR = [
// Kilitli liste satırı / kilit kartı (`listem-govde`)
"kilit",
// Üretimde kredi yetmedi (`liste-uretici`)
"kredi_bitti",
// Üst menü (`user-nav`)
"nav",
// Ana sayfa fiyat bölümü: paket kartı ve kredi (topup) kartı
"fiyat_paket",
"fiyat_kredi",
// Koşullar metnindeki "paket sayfasında" linki
"kosullar",
// "Tercihlerin değişmiş" modalı: aksiyon satırı / kredi-yetersiz satırı
"tercih_degisti",
"tercih_degisti_yetersiz",
// Danışman sohbeti: başlıktaki kredi rozeti, kredi bitti bandı, son krediler
// uyarısı
"danisman_rozet",
"danisman_kredi_bitti",
"danisman_kredi_az",
// Ödeme akışı: checkout şeridindeki geri linki, başarısız sonuç kartı
"odeme_geri",
"odeme_hata",
] as const;
export function PaketGorunumOlayi() {
useEffect(() => {

View File

@@ -40,8 +40,8 @@ import {
import type { Dilim } from "@/lib/rapor-havuzu";
// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli.
// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket dar ekranda sr-only
// olarak kalır (renk tek başına anlam taşımaz).
// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket her ekran
// genişliğinde yazılı durur (renk tek başına anlam taşımaz).
const RISK_STIL: Record<RiskSeviyesi, { nokta: string; icon: typeof Rocket }> = {
riskli: { nokta: "bg-red-500", icon: Rocket },
"az-riskli": { nokta: "bg-amber-500", icon: Scale },
@@ -264,9 +264,7 @@ export function RaporListesi({
className={`size-2.5 shrink-0 rounded-full ${stil.nokta}`}
aria-hidden
/>
<span className="sr-only sm:not-sr-only">
{RISK_ETIKET[risk]}
</span>
<span>{RISK_ETIKET[risk]}</span>
</span>
)}
</TableCell>

View File

@@ -348,7 +348,7 @@ export function SohbetClient({
</button>
) : null}
<Link
href="/paket"
href="/paket?kaynak=danisman_rozet"
className="inline-flex items-center gap-1.5 rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-semibold text-amber-700 transition-colors hover:bg-amber-100"
title="Kredilerin — yüklemek için tıkla"
>
@@ -535,7 +535,7 @@ export function SohbetClient({
size="sm"
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
>
<Link href="/paket">Kredi yükle</Link>
<Link href="/paket?kaynak=danisman_kredi_bitti">Kredi yükle</Link>
</Button>
</div>
) : (
@@ -543,7 +543,7 @@ export function SohbetClient({
{kredi <= 3 ? (
<p className="mb-2 px-1 text-xs text-amber-700">
Son {kredi} kredin — bitse de listen ve sohbetin kaybolmaz;{" "}
<Link href="/paket" className="underline hover:text-amber-800">
<Link href="/paket?kaynak=danisman_kredi_az" className="underline hover:text-amber-800">
kredi yükleyip
</Link>{" "}
kaldığın yerden devam edersin.

View File

@@ -120,9 +120,11 @@ export function TadimlikSatiri({
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`;
return (
// first:mt-0 — /sonuc'ta bu <section> main'in ilk DOM çocuğudur (harita
// artık tablonun altında, üstte hiçbir kardeş yok; JSX yorumları düğüm
// üretmez): main'in py-12'si tek başına yeterli üst boşluğu verir.
// first:mt-0 — /sonuc'ta harita bu <section>'ın ÜSTÜNDE durur ama yalnızca
// kayıtlı listesi olan ziyaretçide DOM'a girer (manuel-harita.tsx). Liste
// yoksa tadımlık main'in ilk DOM çocuğudur (JSX yorumları düğüm üretmez) ve
// main'in py-12'si tek başına yeterli üst boşluğu verir; harita varsa
// tadımlık ikinci çocuk olur ve bölüm ritmini veren mt-16'yı alır.
// Tadımlığın render edilmediği durumlarda (girişli kullanıcı, boş havuz)
// bu bileşen hiç dönmediği için kural ilk gerçek bölüme geçer.
<section ref={gorunumRef} className="mt-16 min-w-0 first:mt-0">

View File

@@ -297,7 +297,7 @@ export function TercihDegistiModali({
asChild
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
>
<Link href="/paket">
<Link href="/paket?kaynak=tercih_degisti">
{hasPaket
? "Kredi yükle ve güncelle"
: "Paketi aktive et"}
@@ -349,7 +349,7 @@ function KrediYetersiz({
asChild
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600"
>
<Link href="/paket">
<Link href="/paket?kaynak=tercih_degisti_yetersiz">
{hasPaket ? "Kredi yükle" : "Paketi aktive et"}
<ArrowRight className="size-4" aria-hidden />
</Link>

View File

@@ -141,6 +141,25 @@ export function SihirbazAdimlar({
() => canliFacetler.iller.filter((i) => HARITA_ILLERI.has(i.il)),
[canliFacetler.iller],
);
// Huninin paydası: 1. adım hiçbir zaman "geçilmediği" için `adimaGit`ten
// atılamıyordu (orası yalnız ileri geçişleri sayar) ve `sihirbaz_adim{1}`
// hiç düşmüyordu — payda `sira_girildi`den türetilmek zorunda kalıyordu.
// Modal kapanınca bileşen unmount olduğu için mount = "1. adım görüldü";
// 1→2→1→2 gidip gelmek unmount etmediğinden tekrar tetiklenmez.
//
// Bilerek `olayBirKez` KULLANILMAZ (bkz. lib/analitik.ts:22-24, aynı kural
// `liste_uretim_basladi` için yazılı): `:220`'deki ileri geçiş olayı
// (`adim:2`, `adim:3`) her geçişte atılıyor. Paydayı oturum başına tekil
// yapsaydık sihirbazı ikinci kez açan öğrencide pay 2, payda 1 kalır ve
// 1→2 geçiş oranı %100'ü aşardı. Payda ile pay aynı birimde olmalı:
// sihirbaz her açıldığında `adim:1` de düşer.
//
// SERİ KIRILMASI: bu satırın canlıya çıktığı tarihten önce `sihirbaz_adim`
// serisinde `adim:1` hiç yok; öncesi/sonrası karşılaştırılamaz.
useEffect(() => {
olay("sihirbaz_adim", { adim: 1 });
}, []);
const [facetYukleniyor, setFacetYukleniyor] = useState(false);
useEffect(() => {
if (adim === 0) return;
@@ -201,7 +220,8 @@ export function SihirbazAdimlar({
setYon(yeniAdim > adim ? 1 : -1);
setGecisBasladi(true);
setAdim(yeniAdim);
// Yalnızca ileri geçişler huni adımı sayılır; geri dönüşler tekrar saymasın
// Yalnızca ileri geçişler huni adımı sayılır; geri dönüşler tekrar saymasın.
// `adim:1` buradan hiç geçmez — onu yukarıdaki mount effect'i atar.
if (yeniAdim > adim) olay("sihirbaz_adim", { adim: yeniAdim + 1 });
}

View File

@@ -10,6 +10,7 @@ import type { Program } from "@/types/yokatlas";
import { kategoriBul, type Kategori } from "./kategoriler";
import { uniAdiNormalize } from "./harita";
import { bolumBazAdi, turkishSlugify, trBaslikDuzeni } from "./slug";
import { urapSirasiBul, type UrapSira } from "./urap-siralama";
/**
* Programatik SEO sayfalarının veri katmanı (/bolum, /universite, hub'lar).
@@ -224,6 +225,11 @@ export type UniOzet = {
};
export type UniDetay = UniOzet & {
/**
* URAP Türkiye genel sıralaması; listede olmayan üniversitede null.
* Gösterildiği her yüzeyde URAP_KAYNAK_URL'ye aktif atıf zorunludur.
*/
urap: UrapSira | null;
fakulteler: { fakulte: string; programlar: Program[] }[];
stats: {
minSira: number | null;
@@ -393,6 +399,7 @@ export function getUniversiteBySlug(slug: string): UniDetay | null {
const ozet = getAllUniversiteler().find((u) => u.slug === slug)!;
return {
...ozet,
urap: urapSirasiBul(slug),
fakulteler: [...gruplar.entries()]
.map(([fakulte, programlar]) => ({ fakulte, programlar }))
.sort((a, b) => a.fakulte.localeCompare(b.fakulte, "tr-TR")),

37
src/lib/unitur.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* Ham `unitur` değerlerinin (DB: DEVLET, VAKIF, VAKIF MYO, KKTC,
* YURTDISI KAMU, YURTDISI VAKIF) ekranda görünen karşılığı — **tek kaynak**.
* `DILIM_ETIKET`'in `lib/risk.ts`'te durması gibi: ham anahtarı kullanıcıya
* basma, buradan oku (AGENTS.md "ortak elemanlar tek kaynaktan").
*
* `trBaslikDuzeni` bu değerlere uygulanamaz: tr-TR küçültmesi "I" → "ı"
* yaptığı için "KKTC" → "Kktc", "VAKIF MYO" → "Vakıf Myo",
* "YURTDISI KAMU" → "Yurtdısı Kamu" gibi Türkçede olmayan kelimeler çıkıyor.
*
* İki biçim var, çünkü aynı bilgi iki bağlamda geçiyor:
* `cip` = başlık altındaki kimlik çipinde okunan kısa etiket,
* `cumle` = özet paragrafın ilk virgülündeki öbek; cümle ortasında
* okunacağı için küçük harfle başlar, KKTC gibi kısaltmalar
* büyük kalır.
*/
export const UNITUR_ETIKET: Record<string, { cip: string; cumle: string }> = {
DEVLET: { cip: "Devlet", cumle: "devlet üniversitesi" },
VAKIF: { cip: "Vakıf", cumle: "vakıf üniversitesi" },
"VAKIF MYO": { cip: "Vakıf MYO", cumle: "vakıf meslek yüksekokulu" },
KKTC: { cip: "KKTC", cumle: "KKTC üniversitesi" },
"YURTDISI KAMU": {
cip: "Yurt dışı (kamu)",
cumle: "yurt dışında bir kamu üniversitesi",
},
"YURTDISI VAKIF": {
cip: "Yurt dışı (vakıf)",
cumle: "yurt dışında bir vakıf üniversitesi",
},
};
/**
* Çipin renk dili: devlet → slate, diğer her şey (vakıf, KKTC, yurt dışı)
* → violet. Sitedeki dört çip kopyasının hepsinde aynı kural.
*/
export const uniturDevletMi = (unitur: string | null | undefined) =>
unitur === "DEVLET";

242
src/lib/urap-siralama.ts Normal file
View File

@@ -0,0 +1,242 @@
// URAP (University Ranking by Academic Performance) Türkiye genel sıralaması.
//
// Kaynak: URAP Araştırma Laboratuvarı, 2025-2026 genel sıralaması
// (URAP_KAYNAK_URL). Veri, URAP'ın yazılı izniyle yayımlanıyor; izin
// "urapcenter.org adresine aktif bağlantı/atıf vermek" şartına bağlı.
// Bu yüzden URAP sayısının göründüğü HER yüzeyde, aynı ekranda,
// URAP_KAYNAK_URL'ye çıkan gerçek bir <a href> bulunmak zorunda
// (rel="noopener", yeni sekme, nofollow YOK). Link konulamayan bir
// yüzeyde (ör. OG görseli) URAP sayısı hiç gösterilmez.
//
// Anahtar biçimi: /universite/[slug] sayfa slug'ı (slug.ts → uniSayfaSlug).
// URAP 198 üniversitenin 198'inde bizim yazımımızla birebir eşleşiyor;
// takma ad tablosu yok. Burada olmayan üniversite URAP listesinde yoktur —
// "kötü" demek değildir (KKTC/yurt dışı kurumlar, yalnız MYO'su olanlar ve
// araştırma çıktısı henüz oluşmamış yeni üniversiteler listeye girmiyor).
//
// Yıllık tazeleme: URAP sıralamayı her Ekim yeni bir adreste yayımlıyor;
// veri ve URAP_KAYNAK_URL aynı commit'te birlikte güncellenir. Denetimi
// `pnpm veri-kalite` içindeki URAP kontrolleri yapar.
/** Sıralamanın dönemi. Ölçtüğü veri dönemi 2020-2024'tür (URAP Yöntem sayfası). */
export const URAP_DONEM = "2025-2026";
/** Sıralamadaki toplam üniversite sayısı — payda. Sıra tek başına gösterilmez. */
export const URAP_TOPLAM = 198;
/** Atıf şartının adresi: sıralamanın o yıla ait kendi sayfası. */
export const URAP_KAYNAK_URL =
"https://newtr.urapcenter.org/Rankings/2025-2026/GENEL-SIRALAMASI-2025";
export type UrapSira = {
/** Türkiye genel sıralamasındaki sıra (1..URAP_TOPLAM) */
sira: number;
/** URAP toplam puanı — sayfada gösterilmez, veri kalitesi kontrolü kullanır */
puan: number;
};
export const URAP_SIRA: Record<string, UrapSira> = {
"koc-universitesi": { sira: 1, puan: 1152.91 },
"hacettepe-universitesi": { sira: 2, puan: 1096.48 },
"orta-dogu-teknik-universitesi": { sira: 3, puan: 1082.81 },
"istanbul-teknik-universitesi": { sira: 4, puan: 1067.01 },
"ankara-universitesi": { sira: 5, puan: 1038.45 },
"gazi-universitesi": { sira: 6, puan: 1006.92 },
"ege-universitesi": { sira: 7, puan: 1003.87 },
"istanbul-universitesi-cerrahpasa": { sira: 8, puan: 998.17 },
"istanbul-universitesi": { sira: 9, puan: 994.98 },
"sabanci-universitesi": { sira: 10, puan: 992.03 },
"yildiz-teknik-universitesi": { sira: 11, puan: 962.25 },
"gebze-teknik-universitesi": { sira: 12, puan: 933.23 },
"ataturk-universitesi": { sira: 13, puan: 932.94 },
"erciyes-universitesi": { sira: 14, puan: 931.72 },
"marmara-universitesi": { sira: 15, puan: 929.21 },
"ihsan-dogramaci-bilkent-universitesi": { sira: 16, puan: 903.76 },
"firat-universitesi": { sira: 17, puan: 885.66 },
"bogazici-universitesi": { sira: 18, puan: 881.20 },
"dokuz-eylul-universitesi": { sira: 19, puan: 869.21 },
"karadeniz-teknik-universitesi": { sira: 20, puan: 866.97 },
"akdeniz-universitesi": { sira: 21, puan: 862.50 },
"izmir-yuksek-teknoloji-enstitusu": { sira: 22, puan: 862.37 },
"acibadem-mehmet-ali-aydinlar-universitesi": { sira: 23, puan: 852.95 },
"cukurova-universitesi": { sira: 24, puan: 851.47 },
"bezm-i-alem-vakif-universitesi": { sira: 25, puan: 841.14 },
"selcuk-universitesi": { sira: 26, puan: 840.50 },
"saglik-bilimleri-universitesi": { sira: 27, puan: 837.93 },
"sakarya-universitesi": { sira: 28, puan: 836.46 },
"ondokuz-mayis-universitesi": { sira: 29, puan: 829.78 },
"bursa-uludag-universitesi": { sira: 30, puan: 828.79 },
"istinye-universitesi": { sira: 31, puan: 824.40 },
"eskisehir-osmangazi-universitesi": { sira: 32, puan: 808.86 },
"kocaeli-universitesi": { sira: 33, puan: 787.14 },
"necmettin-erbakan-universitesi": { sira: 34, puan: 775.91 },
"inonu-universitesi": { sira: 35, puan: 774.31 },
"ankara-yildirim-beyazit-universitesi": { sira: 36, puan: 769.99 },
"izmir-katip-celebi-universitesi": { sira: 37, puan: 763.47 },
"istanbul-medeniyet-universitesi": { sira: 38, puan: 761.73 },
"recep-tayyip-erdogan-universitesi": { sira: 39, puan: 755.54 },
"kadir-has-universitesi": { sira: 40, puan: 748.99 },
"istanbul-medipol-universitesi": { sira: 41, puan: 747.95 },
"suleyman-demirel-universitesi": { sira: 42, puan: 739.79 },
"mersin-universitesi": { sira: 43, puan: 736.04 },
"eskisehir-teknik-universitesi": { sira: 44, puan: 734.81 },
"harran-universitesi": { sira: 45, puan: 732.49 },
"konya-teknik-universitesi": { sira: 46, puan: 731.10 },
"van-yuzuncu-yil-universitesi": { sira: 47, puan: 728.49 },
"bahcesehir-universitesi": { sira: 48, puan: 722.03 },
"gaziantep-universitesi": { sira: 49, puan: 720.53 },
"yeditepe-universitesi": { sira: 50, puan: 720.51 },
"duzce-universitesi": { sira: 51, puan: 711.46 },
"canakkale-onsekiz-mart-universitesi": { sira: 52, puan: 711.34 },
"baskent-universitesi": { sira: 53, puan: 709.49 },
"sivas-cumhuriyet-universitesi": { sira: 54, puan: 708.36 },
"mugla-sitki-kocman-universitesi": { sira: 55, puan: 700.99 },
"abdullah-gul-universitesi": { sira: 56, puan: 698.46 },
"karabuk-universitesi": { sira: 57, puan: 695.29 },
"isparta-uygulamali-bilimler-universitesi": { sira: 58, puan: 692.14 },
"bingol-universitesi": { sira: 59, puan: 690.31 },
"bartin-universitesi": { sira: 60, puan: 690.02 },
"nigde-omer-halisdemir-universitesi": { sira: 61, puan: 686.24 },
"biruni-universitesi": { sira: 62, puan: 686.04 },
"bolu-abant-izzet-baysal-universitesi": { sira: 63, puan: 682.56 },
"manisa-celal-bayar-universitesi": { sira: 64, puan: 681.21 },
"ozyegin-universitesi": { sira: 65, puan: 674.94 },
"kastamonu-universitesi": { sira: 66, puan: 674.08 },
"erzurum-teknik-universitesi": { sira: 67, puan: 672.03 },
"aydin-adnan-menderes-universitesi": { sira: 68, puan: 671.78 },
"tarsus-universitesi": { sira: 69, puan: 669.45 },
"pamukkale-universitesi": { sira: 70, puan: 669.23 },
"tobb-ekonomi-ve-teknoloji-universitesi": { sira: 71, puan: 660.20 },
"anadolu-universitesi": { sira: 72, puan: 656.74 },
"bursa-teknik-universitesi": { sira: 73, puan: 647.49 },
"adana-alparslan-turkes-bilim-ve-teknoloji-universitesi": { sira: 74, puan: 646.96 },
"kahramanmaras-sutcu-imam-universitesi": { sira: 75, puan: 646.28 },
"cankaya-universitesi": { sira: 76, puan: 639.31 },
"siirt-universitesi": { sira: 77, puan: 631.70 },
"karamanoglu-mehmetbey-universitesi": { sira: 78, puan: 628.87 },
"zonguldak-bulent-ecevit-universitesi": { sira: 79, puan: 628.13 },
"tekirdag-namik-kemal-universitesi": { sira: 80, puan: 626.48 },
"atilim-universitesi": { sira: 81, puan: 625.01 },
"aksaray-universitesi": { sira: 82, puan: 624.78 },
"bilecik-seyh-edebali-universitesi": { sira: 83, puan: 624.59 },
"hatay-mustafa-kemal-universitesi": { sira: 84, puan: 621.04 },
"hitit-universitesi": { sira: 85, puan: 620.88 },
"cankiri-karatekin-universitesi": { sira: 86, puan: 619.86 },
"hasan-kalyoncu-universitesi": { sira: 87, puan: 618.15 },
"yozgat-bozok-universitesi": { sira: 88, puan: 611.80 },
"kirikkale-universitesi": { sira: 89, puan: 608.75 },
"erzincan-binali-yildirim-universitesi": { sira: 90, puan: 602.70 },
"trakya-universitesi": { sira: 91, puan: 601.88 },
"tokat-gaziosmanpasa-universitesi": { sira: 92, puan: 595.80 },
"iskenderun-teknik-universitesi": { sira: 93, puan: 594.68 },
"dicle-universitesi": { sira: 94, puan: 594.60 },
"burdur-mehmet-akif-ersoy-universitesi": { sira: 95, puan: 593.54 },
"izmir-ekonomi-universitesi": { sira: 96, puan: 592.96 },
"afyon-kocatepe-universitesi": { sira: 97, puan: 592.85 },
"usak-universitesi": { sira: 98, puan: 589.80 },
"ordu-universitesi": { sira: 99, puan: 582.94 },
"igdir-universitesi": { sira: 100, puan: 577.55 },
"sakarya-uygulamali-bilimler-universitesi": { sira: 101, puan: 565.68 },
"yasar-universitesi": { sira: 102, puan: 561.18 },
"osmaniye-korkut-ata-universitesi": { sira: 103, puan: 558.18 },
"altinbas-universitesi": { sira: 104, puan: 556.66 },
"balikesir-universitesi": { sira: 105, puan: 550.12 },
"adiyaman-universitesi": { sira: 106, puan: 546.48 },
"munzur-universitesi": { sira: 107, puan: 545.61 },
"istanbul-ticaret-universitesi": { sira: 108, puan: 544.68 },
"malatya-turgut-ozal-universitesi": { sira: 109, puan: 542.65 },
"kirsehir-ahi-evran-universitesi": { sira: 110, puan: 541.62 },
"batman-universitesi": { sira: 111, puan: 530.02 },
"istanbul-okan-universitesi": { sira: 112, puan: 527.88 },
"kafkas-universitesi": { sira: 113, puan: 527.04 },
"bayburt-universitesi": { sira: 114, puan: 525.89 },
"bandirma-onyedi-eylul-universitesi": { sira: 115, puan: 524.68 },
"giresun-universitesi": { sira: 116, puan: 520.78 },
"samsun-universitesi": { sira: 117, puan: 516.24 },
"alanya-alaaddin-keykubat-universitesi": { sira: 118, puan: 512.85 },
"konya-gida-ve-tarim-universitesi": { sira: 119, puan: 512.57 },
"nevsehir-haci-bektas-veli-universitesi": { sira: 120, puan: 509.45 },
"sinop-universitesi": { sira: 121, puan: 509.24 },
"gumushane-universitesi": { sira: 122, puan: 507.79 },
"izmir-bakircay-universitesi": { sira: 123, puan: 504.13 },
"antalya-bilim-universitesi": { sira: 124, puan: 503.18 },
"ibn-haldun-universitesi": { sira: 125, puan: 501.66 },
"artvin-coruh-universitesi": { sira: 126, puan: 498.79 },
"istanbul-sabahattin-zaim-universitesi": { sira: 127, puan: 497.58 },
"ted-universitesi": { sira: 128, puan: 496.02 },
"agri-ibrahim-cecen-universitesi": { sira: 129, puan: 493.56 },
"lokman-hekim-universitesi": { sira: 130, puan: 492.28 },
"istanbul-aydin-universitesi": { sira: 131, puan: 489.10 },
"amasya-universitesi": { sira: 132, puan: 486.73 },
"kutahya-saglik-bilimleri-universitesi": { sira: 133, puan: 486.66 },
"istanbul-nisantasi-universitesi": { sira: 134, puan: 479.20 },
"kayseri-universitesi": { sira: 135, puan: 478.40 },
"galatasaray-universitesi": { sira: 136, puan: 476.49 },
"istanbul-bilgi-universitesi": { sira: 137, puan: 476.16 },
"istanbul-gelisim-universitesi": { sira: 138, puan: 475.63 },
"uskudar-universitesi": { sira: 139, puan: 472.03 },
"turk-alman-universitesi": { sira: 140, puan: 471.87 },
"yalova-universitesi": { sira: 141, puan: 464.53 },
"kutahya-dumlupinar-universitesi": { sira: 142, puan: 462.86 },
"istanbul-arel-universitesi": { sira: 143, puan: 459.52 },
"piri-reis-universitesi": { sira: 144, puan: 451.88 },
"afyonkarahisar-saglik-bilimleri-universitesi": { sira: 145, puan: 448.30 },
"izmir-demokrasi-universitesi": { sira: 146, puan: 447.60 },
"kto-karatay-universitesi": { sira: 147, puan: 440.94 },
"mus-alparslan-universitesi": { sira: 148, puan: 439.94 },
"maltepe-universitesi": { sira: 149, puan: 431.24 },
"ardahan-universitesi": { sira: 150, puan: 422.37 },
"kirklareli-universitesi": { sira: 151, puan: 421.10 },
"bitlis-eren-universitesi": { sira: 152, puan: 414.54 },
"demiroglu-bilim-universitesi": { sira: 153, puan: 407.06 },
"ankara-haci-bayram-veli-universitesi": { sira: 154, puan: 404.20 },
"turk-hava-kurumu-universitesi": { sira: 155, puan: 398.48 },
"fatih-sultan-mehmet-vakif-universitesi": { sira: 156, puan: 392.99 },
"dogus-universitesi": { sira: 157, puan: 388.75 },
"sirnak-universitesi": { sira: 158, puan: 377.20 },
"trabzon-universitesi": { sira: 159, puan: 376.52 },
"halic-universitesi": { sira: 160, puan: 375.33 },
"mef-universitesi": { sira: 161, puan: 373.15 },
"gaziantep-islam-bilim-ve-teknoloji-universitesi": { sira: 162, puan: 367.89 },
"ostim-teknik-universitesi": { sira: 163, puan: 363.20 },
"sanko-universitesi": { sira: 164, puan: 361.18 },
"isik-universitesi": { sira: 165, puan: 360.25 },
"istanbul-beykent-universitesi": { sira: 166, puan: 360.02 },
"mardin-artuklu-universitesi": { sira: 167, puan: 358.73 },
"ankara-medipol-universitesi": { sira: 168, puan: 349.71 },
"hakkari-universitesi": { sira: 169, puan: 348.80 },
"ufuk-universitesi": { sira: 170, puan: 344.76 },
"kilis-7-aralik-universitesi": { sira: 171, puan: 342.59 },
"kahramanmaras-istiklal-universitesi": { sira: 172, puan: 340.49 },
"istanbul-gedik-universitesi": { sira: 173, puan: 337.25 },
"ankara-sosyal-bilimler-universitesi": { sira: 174, puan: 333.58 },
"istanbul-kultur-universitesi": { sira: 175, puan: 330.47 },
"yuksek-ihtisas-universitesi": { sira: 176, puan: 318.38 },
"cag-universitesi": { sira: 177, puan: 318.17 },
"mimar-sinan-guzel-sanatlar-universitesi": { sira: 178, puan: 315.16 },
"istanbul-yeni-yuzyil-universitesi": { sira: 179, puan: 298.64 },
"istanbul-saglik-ve-teknoloji-universitesi": { sira: 180, puan: 297.39 },
"nuh-naci-yazgan-universitesi": { sira: 181, puan: 269.70 },
"istanbul-atlas-universitesi": { sira: 182, puan: 248.03 },
"fenerbahce-universitesi": { sira: 183, puan: 243.09 },
"toros-universitesi": { sira: 184, puan: 241.33 },
"kapadokya-universitesi": { sira: 185, puan: 237.16 },
"izmir-tinaztepe-universitesi": { sira: 186, puan: 235.88 },
"istanbul-kent-universitesi": { sira: 187, puan: 231.72 },
"istanbul-rumeli-universitesi": { sira: 188, puan: 179.54 },
"avrasya-universitesi": { sira: 189, puan: 178.53 },
"alanya-universitesi": { sira: 190, puan: 176.69 },
"istanbul-topkapi-universitesi": { sira: 191, puan: 171.16 },
"istanbul-29-mayis-universitesi": { sira: 192, puan: 170.22 },
"ankara-bilim-universitesi": { sira: 193, puan: 154.45 },
"kocaeli-saglik-ve-teknoloji-universitesi": { sira: 194, puan: 116.23 },
"istanbul-galata-universitesi": { sira: 195, puan: 114.92 },
"istanbul-esenyurt-universitesi": { sira: 196, puan: 97.81 },
"beykoz-universitesi": { sira: 197, puan: 83.48 },
"antalya-belek-universitesi": { sira: 198, puan: 70.82 },
};
/** Üniversite sayfa slug'ının URAP sırası; listede yoksa null. */
export function urapSirasiBul(slug: string): UrapSira | null {
return URAP_SIRA[slug] ?? null;
}