diff --git a/data/yokatlas.db b/data/yokatlas.db index 302d2af..3926e72 100644 Binary files a/data/yokatlas.db and b/data/yokatlas.db differ diff --git a/docs/gece-vardiyasi/2026-09-22/07-p1-risk-etiketi.md b/docs/gece-vardiyasi/2026-09-22/07-p1-risk-etiketi.md new file mode 100644 index 0000000..cf91447 --- /dev/null +++ b/docs/gece-vardiyasi/2026-09-22/07-p1-risk-etiketi.md @@ -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ığı: `` → `Risk`. +- `RISK_NOKTA`'nın üstüne renk dili yorumu eklendi (rapor-listesi'ndeki yorumun eşi). + +### `src/features/rapor/components/rapor-listesi.tsx` +- `:267` `` → ``; 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ı. diff --git a/docs/gece-vardiyasi/2026-09-22/08-p4-olcum.md b/docs/gece-vardiyasi/2026-09-22/08-p4-olcum.md new file mode 100644 index 0000000..27dffdc --- /dev/null +++ b/docs/gece-vardiyasi/2026-09-22/08-p4-olcum.md @@ -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. diff --git a/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md b/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md new file mode 100644 index 0000000..21353b9 --- /dev/null +++ b/docs/gece-vardiyasi/2026-09-22/09-p2-unitur.md @@ -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 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.** diff --git a/docs/gece-vardiyasi/2026-09-22/10-p3-urap.md b/docs/gece-vardiyasi/2026-09-22/10-p3-urap.md new file mode 100644 index 0000000..9b714db --- /dev/null +++ b/docs/gece-vardiyasi/2026-09-22/10-p3-urap.md @@ -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), `` + +## Ö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 `` 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: ``; 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//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 ? : 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//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. diff --git a/scripts/unitur-onar.ts b/scripts/unitur-onar.ts index f192d52..f4bdb02 100644 --- a/scripts/unitur-onar.ts +++ b/scripts/unitur-onar.ts @@ -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 = { "İ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)"); - const dagilim = db - .prepare( - "SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM programs 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(" ")); + 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 ${tablo} GROUP BY unitur ORDER BY n DESC` + ) + .all() as { t: string; n: number }[]; + console.log( + `Dağılım (${tablo}):`, + dagilim.map((d) => `${d.t}=${d.n}`).join(" ") + ); + } db.close(); } diff --git a/scripts/veri-kalite.ts b/scripts/veri-kalite.ts index 862e848..1e0cd46 100644 --- a/scripts/veri-kalite.ts +++ b/scripts/veri-kalite.ts @@ -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(); + 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(", ")}` + ); +} diff --git a/src/app/kosullar/page.tsx b/src/app/kosullar/page.tsx index 7a318fd..f644c17 100644 --- a/src/app/kosullar/page.tsx +++ b/src/app/kosullar/page.tsx @@ -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{" "} - + paket sayfasında {" "} yazar. diff --git a/src/app/page.tsx b/src/app/page.tsx index fd8807f..90ed73b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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" > - + {PAKET_FIYATI} TL — Paketi incele @@ -570,7 +570,7 @@ export default function Home() { variant="outline" className="mt-4 h-11 w-full cursor-pointer" > - + +{URUNLER.topup.credits} kredi — {TOPUP_FIYATI} TL diff --git a/src/app/sonuc/page.tsx b/src/app/sonuc/page.tsx index b30fe24..dbd7600 100644 --- a/src/app/sonuc/page.tsx +++ b/src/app/sonuc/page.tsx @@ -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 (
- {/* 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. */} + {/* 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} /> - - {/* Sihirbaz akışı (CTA + modal) — üretim ve liste /listem'de yaşar, burada yalnızca funnel kartları render edilir */} - {devlet ? "Devlet" : trBaslikDuzeni(p.unitur)} + {UNITUR_ETIKET[p.unitur]?.cip ?? + trBaslikDuzeni(p.unitur)} ) : null} diff --git a/src/features/katalog/components/universite-icerik.tsx b/src/features/katalog/components/universite-icerik.tsx index 3769ae6..ef5f258 100644 --- a/src/features/katalog/components/universite-icerik.tsx +++ b/src/features/katalog/components/universite-icerik.tsx @@ -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 , 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 ( +

+ {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.`} +

+ ); + } + return ( +

+ 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:{" "} + + urapcenter.org + + . +

+ ); +} + 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({
+ {/* 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 ? ( +

+ + {turEtiketi.cip} + + {uni.il ? ( + {trBaslikDuzeni(uni.il)} + ) : null} +

+ ) : null} + {ilkSayfa ? ( <>

{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{" "} {sayi(uni.programSayisi)} programda öğ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)} + {/* Ş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. */}

-
- Tür -
-
- {uni.unitur === "DEVLET" - ? "Devlet" - : uni.unitur - ? trBaslikDuzeni(uni.unitur) - : "—"} -
+ {uni.urap ? ( + <> +
+ URAP Türkiye +
+
+ {sayi(uni.urap.sira)} / {sayi(URAP_TOPLAM)} + + {URAP_DONEM} + +
+ + ) : ( + <> +
+ Toplam kontenjan +
+
+ {sayi(uni.stats.toplamKontenjan)} +
+ + )}
@@ -371,6 +473,8 @@ export function UniversiteIcerik({ veya o yıl yerleşen çıkmayan program).{" "} + {ilkSayfa ? : null} + = { @@ -20,8 +20,9 @@ const RISK_AGIRLIK: Record = { riskli: 2, }; -export function ManuelHarita() { +export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) { const liste = useManuelListe(); + const hidre = useListeHidreMi(); const [seciliIl, setSeciliIl] = useState(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
'ı 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 ( -
+
= { guvenli: "bg-emerald-500", "az-riskli": "bg-amber-500", @@ -237,7 +245,7 @@ export function ProgramTablosu({ - + Risk Program Son {VERI_YIL_SAYISI} yıl @@ -304,12 +312,15 @@ function ProgramSatiri({ return ( - + {risk ? ( - + + + {RISK_ETIKET[risk]} + ) : null} diff --git a/src/features/liste/components/tercih-haritasi-lazy.tsx b/src/features/liste/components/tercih-haritasi-lazy.tsx index d6f1a97..7ef9125 100644 --- a/src/features/liste/components/tercih-haritasi-lazy.tsx +++ b/src/features/liste/components/tercih-haritasi-lazy.tsx @@ -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 (
); } diff --git a/src/features/liste/components/yurtdisi-haritalar-lazy.tsx b/src/features/liste/components/yurtdisi-haritalar-lazy.tsx index fc69096..bf13c92 100644 --- a/src/features/liste/components/yurtdisi-haritalar-lazy.tsx +++ b/src/features/liste/components/yurtdisi-haritalar-lazy.tsx @@ -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: () =>
}, ); diff --git a/src/features/liste/liste-dogrula.ts b/src/features/liste/liste-dogrula.ts index 65674ce..2396a66 100644 --- a/src/features/liste/liste-dogrula.ts +++ b/src/features/liste/liste-dogrula.ts @@ -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 = { diff --git a/src/features/liste/liste-store.ts b/src/features/liste/liste-store.ts index 26167dc..5464c92 100644 --- a/src/features/liste/liste-store.ts +++ b/src/features/liste/liste-store.ts @@ -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, diff --git a/src/features/odeme/components/odeme-kromu.tsx b/src/features/odeme/components/odeme-kromu.tsx index 12689ba..9431cbd 100644 --- a/src/features/odeme/components/odeme-kromu.tsx +++ b/src/features/odeme/components/odeme-kromu.tsx @@ -42,7 +42,7 @@ export function OdemeUstSerit() {
diff --git a/src/features/odeme/components/odeme-sonuc-karti.tsx b/src/features/odeme/components/odeme-sonuc-karti.tsx index 2a872e3..a7911a5 100644 --- a/src/features/odeme/components/odeme-sonuc-karti.tsx +++ b/src/features/odeme/components/odeme-sonuc-karti.tsx @@ -138,7 +138,7 @@ export async function OdemeSonucKarti({ siparis }: { siparis?: string }) {

); diff --git a/src/features/odeme/components/paket-gorunum-olayi.tsx b/src/features/odeme/components/paket-gorunum-olayi.tsx index 8d9a172..032ed8b 100644 --- a/src/features/odeme/components/paket-gorunum-olayi.tsx +++ b/src/features/odeme/components/paket-gorunum-olayi.tsx @@ -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(() => { diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index 0a59dc7..16db174 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -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 = { 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 /> - - {RISK_ETIKET[risk]} - + {RISK_ETIKET[risk]} )}
diff --git a/src/features/rapor/components/sohbet-client.tsx b/src/features/rapor/components/sohbet-client.tsx index 5e317a2..33d8bd3 100644 --- a/src/features/rapor/components/sohbet-client.tsx +++ b/src/features/rapor/components/sohbet-client.tsx @@ -348,7 +348,7 @@ export function SohbetClient({ ) : null} @@ -535,7 +535,7 @@ export function SohbetClient({ size="sm" className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600" > - Kredi yükle + Kredi yükle ) : ( @@ -543,7 +543,7 @@ export function SohbetClient({ {kredi <= 3 ? (

Son {kredi} kredin — bitse de listen ve sohbetin kaybolmaz;{" "} - + kredi yükleyip {" "} kaldığın yerden devam edersin. diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 7caa8ec..276b5b1 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -120,9 +120,11 @@ export function TadimlikSatiri({ const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`; return ( - // first:mt-0 — /sonuc'ta bu

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
'ı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.
diff --git a/src/features/rapor/components/tercih-degisti-modali.tsx b/src/features/rapor/components/tercih-degisti-modali.tsx index 837dec3..1073ffd 100644 --- a/src/features/rapor/components/tercih-degisti-modali.tsx +++ b/src/features/rapor/components/tercih-degisti-modali.tsx @@ -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" > - + {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" > - + {hasPaket ? "Kredi yükle" : "Paketi aktive et"} diff --git a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx index 5f3579c..71ca9fd 100644 --- a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx +++ b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx @@ -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 }); } diff --git a/src/lib/katalog.ts b/src/lib/katalog.ts index ae4339c..fee4430 100644 --- a/src/lib/katalog.ts +++ b/src/lib/katalog.ts @@ -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")), diff --git a/src/lib/unitur.ts b/src/lib/unitur.ts new file mode 100644 index 0000000..137775a --- /dev/null +++ b/src/lib/unitur.ts @@ -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 = { + 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"; diff --git a/src/lib/urap-siralama.ts b/src/lib/urap-siralama.ts new file mode 100644 index 0000000..eb301de --- /dev/null +++ b/src/lib/urap-siralama.ts @@ -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 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 = { + "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; +}