Compare commits
6 Commits
988b9d5ce0
...
70423853f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70423853f3 | ||
|
|
5a04db7e4b | ||
|
|
aa2051732d | ||
|
|
c23910cc79 | ||
|
|
f5e8e4f7c4 | ||
|
|
05402f96f6 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -49,3 +49,6 @@ __pycache__/
|
|||||||
|
|
||||||
# YÖK Atlas ham API çekimleri (scripts/refresh.ts; ~yıllık, büyük, yeniden üretilebilir)
|
# YÖK Atlas ham API çekimleri (scripts/refresh.ts; ~yıllık, büyük, yeniden üretilebilir)
|
||||||
data/ham/
|
data/ham/
|
||||||
|
|
||||||
|
# YÖK Atlas API ham dökümleri (refresh.ts, program-ozellik.ts)
|
||||||
|
/data/ham/
|
||||||
|
|||||||
BIN
data/yokatlas.db
BIN
data/yokatlas.db
Binary file not shown.
132
docs/arastirma/2026-09-25-kisisellestirme/README.md
Normal file
132
docs/arastirma/2026-09-25-kisisellestirme/README.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Kişiselleştirme araştırması: öğrenciden ne alırız, üniversiteyle nasıl eşleştiririz
|
||||||
|
|
||||||
|
> 25 Eylül 2026. Bilal'in onayıyla kaydedildi. Karar: v1 (ana dal) küçük veri işleri, v2 dalı araç kullanımı (tool use) + profil. Aşağıdaki §0, §8 ve §9 onay sohbetinde eklendi.
|
||||||
|
|
||||||
|
## 0. Karar özeti (25 Eyl gece)
|
||||||
|
|
||||||
|
- **Güncellik kuralı:** yılı gösterilemeyen ya da üç yıldan eski veri ürüne girmez. ÜNİ-VERİ (2014-2019 mezunları) bu kuralla elenir; "İş bulma kolaylığı" ve "Yüksek maaş" çipleri güncel kaynak bulunana kadar kalkar. TÜMA 2025, URAP ve YÖK Atlas API'si kalır.
|
||||||
|
- **Düz LLM'e karşı farkımız:** program düzeyinde güncel YÖK Atlas verisi + öğrenciye ait yapılandırılmış, kalıcı profil. İkisi de ChatGPT'de yok; Dalga 1 ve 2 bu yüzden birlikte "biz olma" sebebi.
|
||||||
|
- **v1 (main):** kız/erkek oranı üniversite sayfasında bilgi satırı olarak (yapıldı, 05402f9).
|
||||||
|
- **v2 (dal `v2`):** Dalga 1 (API alanlarını saklama) + Dalga 2 (danışman araçları + `profil_guncelle`) + gizlilik metni güncellemesi.
|
||||||
|
- **Sihirbaza soru eklenmez** (şimdilik): yeni tercihler önce danışmanda toplanır; sık sorulan sihirbaza sonra girer.
|
||||||
|
- **Geçiş soran öğrenciye danışman açılmaz:** Ek Madde-1 hesabı ayrı sayfa olur (veri DB'de, SEO değeri var). DGS verisi yok, sonraya.
|
||||||
|
|
||||||
|
## Tek cümlelik teşhis
|
||||||
|
|
||||||
|
Liste bugün kişisel görünmüyor, çünkü öğrenciden aldığımız 7 "öncelik" çipinin 6'sının arkasında veri yok: "İş bulma kolaylığı", "Yüksek maaş", "Yurtdışı imkânı", "Prestijli üniversite", "Akademik kariyer", "Sevdiğim işi yapmak" yalnızca yapay zekâya metin olarak gidiyor ve model tahmin ediyor. Yalnız "Kolay yerleşme" kodda bir şey değiştiriyor (`src/lib/tercih-degerlendirme.ts:59`). Doğrulama: `src/features/sihirbaz/sihirbaz-sabitler.ts:5` "öncelikler statik kalır (veriye bağlı değil)" diye not düşmüş; `scripts/refresh.ts:181` API'den gelen burs alanını `null` ile atıyor.
|
||||||
|
|
||||||
|
## 1. Bugün öğrenciden ne alıyoruz, neyle eşleştiriyoruz
|
||||||
|
|
||||||
|
| Öğrenciden | Nerede kullanılıyor | Veri karşılığı |
|
||||||
|
|---|---|---|
|
||||||
|
| Sıra + puan türü | Havuz filtresi, dilimler | `programs.sira*` |
|
||||||
|
| İlgi alanı (kategori) | Havuz filtresi (JS) | `src/lib/kategoriler.ts` |
|
||||||
|
| İl (en çok 5) | Havuz filtresi (SQL) | `programs.il` |
|
||||||
|
| Devlet / vakıf / fark etmez | Havuz filtresi | `programs.unitur` |
|
||||||
|
| Öncelikler (7 çip) | Yalnız prompt metni | **Yok** (1 tanesi hariç) |
|
||||||
|
|
||||||
|
Kaynak: `src/features/sihirbaz/sihirbaz-sabitler.ts`, `src/lib/rapor-havuzu.ts` (havuza giden satır: ad, üniversite, tür, il, dilim, sıra geçmişi, kontenjan — başka özellik yok).
|
||||||
|
|
||||||
|
Ayrıca: "Vakıf (burslu olursa)" seçeneği var ama burs türü ve ücret DB'de yok; seçim burslu/ücretli ayırt etmiyor.
|
||||||
|
|
||||||
|
## 2. Üniversite/program tarafında elimizde olan ve eksik veri
|
||||||
|
|
||||||
|
**Asıl bulgu:** YÖK Atlas'ın açık API'si (`/api/tercih-kilavuz/search`, zaten `scripts/refresh.ts` ile çekiyoruz) bizim atıp saklamadığımız çok değerli alanlar veriyor. 25 Eylül'de 200 kayıtla kontrol edildi:
|
||||||
|
|
||||||
|
| Alan (API) | Anlamı | Doluluk (200 kayıtta) | Bizde |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ucret` | Vakıf yıllık ücreti (TL) | 121 | Yok → v2 |
|
||||||
|
| `bursOraniAdi` | Burslu / %50 / %25 / Ücretli | 128 | Yok → v2 |
|
||||||
|
| `ogrenimDiliAdi` | Türkçe / İngilizce … | 199 | Yok → v2 |
|
||||||
|
| `ogrenimTuruAdi` | Örgün / İkinci / Uzaktan | 200 | Yok → v2 |
|
||||||
|
| `akreditasyon`, `uniAkreditasyon` | MÜDEK, TEPDAD, YÖKAK… | 83 / 169 | Yok → v2 |
|
||||||
|
| `prof`, `doc`, `dou`, `arGor` | Programın kadrosu | ~160-190 | Yok → v2 |
|
||||||
|
| `kosulList` | Yerleşme koşulları (sağlık şartı, mülakat, yalnız kız/erkek alan program…) | 197 | Yok → v2 |
|
||||||
|
| `minBasariSirasi` | Tercih için baraj sırası | 70 | Yok → v2 |
|
||||||
|
| `ilceAdi` | Kampüs ilçesi | 199 | Yok → v2 |
|
||||||
|
| `kpss1`, `tusktp`, `abayp` | Mezun sınav ortalamaları (anlamı **doğrulanmalı**) | 50 / 6 / 5 | Yok, alınmadı |
|
||||||
|
|
||||||
|
Diğer kaynaklar:
|
||||||
|
|
||||||
|
| Veri | Kaynak | Düzey | Durum |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Kız/erkek oranı** | YÖK İstatistik Excel (`data/kaynak/…ogrenci-sayilari-2025-2026.xls`, E/K sütunları) | Üniversite × örgün ön lisans+lisans | **v1'de yapıldı** (`kadinYuzdesi`, üniversite sayfası) |
|
||||||
|
| Kız/erkek, program düzeyinde | Eski YÖK Atlas paneli | Program | **Yok** — yeni YÖK Atlas'ta panel kodu duruyor ama menüde değil; üçüncü taraf sitelere güvenilmez |
|
||||||
|
| Öğrenci sayısı, öğretim üyesi başına öğrenci | YÖK İstatistik | Üniversite | Var (`src/lib/yok-istatistik.ts`) |
|
||||||
|
| Araştırma sırası | URAP | Üniversite | Var (`src/lib/urap-siralama.ts`) |
|
||||||
|
| Evden uzaklık | Koordinatlar | Üniversite/il | Var (`src/lib/uni-koordinatlar.ts`) |
|
||||||
|
| Taban oynaklığı, dolmama | `programs` | Program | Var (`src/lib/trend.ts`) |
|
||||||
|
| İş bulma süresi, istihdam, başlangıç ücreti | ÜNİ-VERİ | Bölüm | **Elendi** (2014-2019, güncellik kuralı) |
|
||||||
|
| Öğrenci memnuniyeti | TÜMA 2025 (ÜniAr) | Üniversite, 200 kurum | Yok; yılda bir elle |
|
||||||
|
| Şehir büyüklüğü | TÜİK il nüfusu | İl | Yok; 81 satır elle |
|
||||||
|
| Erasmus / yurtdışı | — | — | Açık kaynak bulunamadı |
|
||||||
|
| KYK yurt kapasitesi | KYGM | Şehir | Yok |
|
||||||
|
|
||||||
|
## 3. Öğrenciden nasıl alırız — üç kanal
|
||||||
|
|
||||||
|
**A. Sihirbaz (herkes, girişsiz).** Şimdilik soru eklenmez (§0). Eklenecekse en fazla 2-3 soru, tek dokunuş, "fark etmez" varsayılanlı: evden uzaklık, vakıfta ödeyebileceği, İngilizce eğitim.
|
||||||
|
|
||||||
|
Öncelik çipleri veriye bağlananlarla yenilenir: "Prestij" → URAP + taban sırası; "Kalabalık olmayan kampüs" → öğrenci sayısı; "Evden yakın" → km; "Akreditasyonlu" → API. "İş bulma" ve "Yüksek maaş" kalkar. Kız/erkek dengesi çip değil bilgi olarak kalır (§5).
|
||||||
|
|
||||||
|
**B. Danışman sohbeti (tool use'un asıl yeri).** Öğrenci serbest konuşur: "İstanbul dışına çıkmam", "yıllık 300 bin üstü olmaz", "kalabalık okul istemem". İki tür araç:
|
||||||
|
- **Sorgu araçları** (salt okunur): program ara/detay, üniversite profili (kız/erkek oranı dahil). Model sayı uydurmaz, veriden okur.
|
||||||
|
- **`profil_guncelle` aracı:** model konuşmadan çıkardığı tercihi **kapalı listeli** alanlara yazar (§8). Öğrenciye "Şunu kaydettim" diye gösterilir, tek tıkla silinebilir. Bir sonraki liste bu profili kullanır.
|
||||||
|
Yapay zekâ listeyi araçla kurmaz — liste üretimi deterministik havuzda kalır (havuz-dışına-çıkamama kuralı korunur).
|
||||||
|
|
||||||
|
**C. Örtük sinyal (sonraya).** /sonuc'ta çıkarılan/eklenen satırlar, ziyaret edilen üniversite sayfaları. KVKK ve karmaşıklık yüzünden ilk turda yok.
|
||||||
|
|
||||||
|
## 4. Nasıl eşleştiririz — üç katman, puanı kod hesaplar
|
||||||
|
|
||||||
|
1. **Sert filtre (SQL):** sıra bandı, puan türü, il ya da km sınırı, üniversite tipi, dil, ücret üst sınırı, öğretim türü, öğrenciye uymayan yerleşme koşulu.
|
||||||
|
2. **Uyum puanı (0-100, kod):** her boyut için 0-1 arası alt puan × öğrencinin ağırlığı. **Verisi olmayan program cezalandırılmaz** (nötr puan, "veri yok" etiketi). Dilim (hayal/dengeli/güvenli) bu puandan etkilenmez.
|
||||||
|
3. **Yapay zekâ:** yalnız havuzdan seçer ve gerekçe yazar; gerekçeye kod üretimi "bu programı öne çıkaran veriler: 180 km, İngilizce, akreditasyonlu" satırı eklenir.
|
||||||
|
|
||||||
|
Filtre gevşetme sırası değişirse `/meraklisina` şeması aynı işte güncellenir (AGENTS.md kuralı).
|
||||||
|
|
||||||
|
## 5. Kız/erkek oranı: nasıl sunulmalı
|
||||||
|
|
||||||
|
- Üniversite sayfasında **bilgi** olarak (v1'de yapıldı): öğrenci karesinde "%54 kadın · %46 erkek", kaynak notunda üniversite düzeyi olduğu yazılı.
|
||||||
|
- **Filtre ya da çip yapılmaz:** cinsiyet sormak gerekir, ayrımcı okunabilir ve oran üniversite düzeyinde. Danışman bu sınırı söyler (araç çıktısında not var).
|
||||||
|
|
||||||
|
## 6. Yatay / dikey geçiş
|
||||||
|
|
||||||
|
- **Yatay (Ek Madde-1):** puan + hedef bölüm → başvurabileceği programlar bugünden hesaplanabilir. Karar: danışman kapısı açılmaz, ayrı sayfa. Kurallar: `docs/arastirma/2026-09-24-ogrenci-senaryolari/07-universite-yatay-gecis-selin.md`.
|
||||||
|
- **Dikey (DGS):** ÖSYM Tablo-2 ve DGS taban puanı gerekir, ikisi de yok (`08-onlisans-dgs-emre.md`). Ayrı tur.
|
||||||
|
|
||||||
|
## 7. Dalgalar
|
||||||
|
|
||||||
|
| Dalga | İş | Durum |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | API alanlarını (ücret, burs, dil, öğretim türü, akreditasyon, koşul, kadro, ilçe, baraj) `program_ozellik` tablosuna al | v2 dalı |
|
||||||
|
| 2 | Danışmana sorgu araçları + `profil_guncelle`, profil görünür/silinebilir, gizlilik metni | v2 dalı |
|
||||||
|
| 3 | Öncelik çiplerini veriye bağlama, uyum puanı + "neden bu program" satırı | Sonra |
|
||||||
|
| 4 | TÜMA, il nüfusu; Ek Madde-1 sayfası; DGS Tablo-2 | Sonra |
|
||||||
|
|
||||||
|
## 8. KVKK: ne yapılır
|
||||||
|
|
||||||
|
Bugün gizlilik metni (`src/app/gizlilik/page.tsx`, 2 Ağustos 2026) e-posta, sıra, sihirbaz seçimleri, sohbet mesajları, sipariş ve Anthropic'e yurt dışı aktarımı sayıyor. Kişiselleştirme için:
|
||||||
|
|
||||||
|
1. **Profil alanları kapalı liste.** `profil_guncelle` yalnız tanımlı alanlara yazar: azami km, yıllık ücret üstü, dil, öğretim türü, kampüs büyüklüğü, ağırlıklar. Serbest metin alanı yok. Aksi hâlde model sohbetten din, sağlık, aile durumu gibi özel nitelikli veriyi profile taşır.
|
||||||
|
2. **Sorulmayanlar:** cinsiyet, sağlık/engel, gelir. Ücret üst sınırı sorulur, gelir sorulmaz. Sağlık koşulu programın yanında gösterilir, öğrenciye durumu sorulmaz.
|
||||||
|
3. **Konum:** il (varsa ilçe), adres/koordinat değil; km il merkezinden.
|
||||||
|
4. **Öğrenci profili görür ve siler.** Her alan ekranda, tek tıkla silinir.
|
||||||
|
5. **Saklama süresi yazılır:** profil ve sohbet tercih dönemi bitiminden 12 ay sonra silinir/anonimleştirilir; silen betik olur.
|
||||||
|
6. **Aydınlatma metni güncellenir:** yeni alanlar + "sohbette söylediğin tercihleri yapılandırılmış bir profile kaydederiz; profil sana gösterilir ve silebilirsin"; yurt dışı aktarım paragrafı profili de kapsar. (Not: metin Anthropic diyor, kod OpenRouter/DeepSeek kullanıyor — düzeltilmeli.)
|
||||||
|
7. **Analitiğe profil gitmez:** Rybbit'e değer değil sayım.
|
||||||
|
8. **VERBİS:** çalışan/bilanço eşiğinin altındaysa muhtemelen yükümlülük yok; mali müşavire sorulur.
|
||||||
|
|
||||||
|
## 9. Başarı ölçüsü
|
||||||
|
|
||||||
|
- Her satırdaki "neden bu program" cümlesi kod üretimi olur; erken benimseyiciye eski-yeni liste yan yana gösterilir.
|
||||||
|
- Danışmanda hangi profil alanının ne sıklıkla dolduğu Rybbit'te sayım olarak izlenir (değer değil).
|
||||||
|
|
||||||
|
## Açık sorular (cevaplandı, 25 Eyl)
|
||||||
|
|
||||||
|
1. Dalga 1 ile başla → evet. 2. Sihirbaza soru → şimdilik hayır. 3. Geçiş soran öğrenciye danışman → hayır, ayrı sayfa.
|
||||||
|
|
||||||
|
## Kaynaklar
|
||||||
|
|
||||||
|
- YÖK Atlas API alan taraması: 25 Eylül 2026, 21.493 kaydın ilk 200'ü.
|
||||||
|
- [TÜMA 2025 (TEDÜ)](https://library.tedu.edu.tr/en/whats-happening-tedu/2025-turkish-university-satisfaction-survey-tuma-results), [ÜniAr](https://uniar.net/en/)
|
||||||
|
- [YÖK Yükseköğretim İstatistikleri](https://veriyonetim.yok.gov.tr/tr/page/507)
|
||||||
|
- Sohbet tabanlı öneri sistemlerinde yapılandırılmış profil: [arXiv 2502.14541](https://arxiv.org/html/2502.14541v1), [arXiv 2608.06632](https://arxiv.org/html/2608.06632v1)
|
||||||
6
drizzle/0005_uneven_omega_red.sql
Normal file
6
drizzle/0005_uneven_omega_red.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `danisman_profil` (
|
||||||
|
`user_id` text PRIMARY KEY NOT NULL,
|
||||||
|
`alanlar` text NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
1011
drizzle/meta/0005_snapshot.json
Normal file
1011
drizzle/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
|||||||
"when": 1790031029618,
|
"when": 1790031029618,
|
||||||
"tag": "0004_shocking_caretaker",
|
"tag": "0004_shocking_caretaker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1790287503094,
|
||||||
|
"tag": "0005_uneven_omega_red",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"lint": "eslint && pnpm tasarim:denetim",
|
"lint": "eslint && pnpm tasarim:denetim",
|
||||||
"ingest": "tsx scripts/ingest.ts",
|
"ingest": "tsx scripts/ingest.ts",
|
||||||
"refresh": "tsx scripts/refresh.ts",
|
"refresh": "tsx scripts/refresh.ts",
|
||||||
|
"ozellik": "tsx scripts/program-ozellik.ts",
|
||||||
"detay": "tsx scripts/detay.ts",
|
"detay": "tsx scripts/detay.ts",
|
||||||
"yokatlas:goc": "tsx scripts/yokatlas-goc.ts",
|
"yokatlas:goc": "tsx scripts/yokatlas-goc.ts",
|
||||||
"gecmis-doldur": "tsx scripts/gecmis-doldur.ts",
|
"gecmis-doldur": "tsx scripts/gecmis-doldur.ts",
|
||||||
|
|||||||
282
scripts/program-ozellik.ts
Normal file
282
scripts/program-ozellik.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* YÖK Atlas API'sindeki program ÖZELLİKLERİNİ `program_ozellik` tablosuna yazar:
|
||||||
|
* yıllık ücret, burs oranı, öğretim dili, öğretim türü, program ve üniversite
|
||||||
|
* akreditasyonu, kadro (prof/doç/dr. öğr. üyesi/arş. gör.), tercih baraj
|
||||||
|
* sırası, kampüs ilçesi ve yerleşme koşulu kodları. Koşul metinleri koda göre
|
||||||
|
* `kosul` tablosunda tek kopya durur.
|
||||||
|
*
|
||||||
|
* refresh.ts'ten AYRI betik: bu alanların yıl anlamı yok (kılavuzun o anki
|
||||||
|
* hâli), yıl kilidi gerekmez; ama --yil ve --db yine zorunlu ve tablo satırı
|
||||||
|
* hangi yılın kılavuzundan geldiğini `yil` kolonunda taşır. Yalnız `programs`
|
||||||
|
* tablosunda olan id'lere yazılır; eşleşmeyen kayıt sayısı raporlanır.
|
||||||
|
*
|
||||||
|
* Kaynağa nazik: sayfa başı 500 kayıt, istekler arası 0,5 sn, tek bağlantı.
|
||||||
|
* Çekilen ham kayıtlar data/ham/ozellik-<yil>-<tarih>.jsonl.gz'e yazılır;
|
||||||
|
* `--ham <dosya>` verilirse API'ye gidilmez, o dosya okunur.
|
||||||
|
*
|
||||||
|
* Kullanım:
|
||||||
|
* pnpm ozellik --yil 2026 --db /tmp/deneme.db
|
||||||
|
* pnpm ozellik --yil 2026 --db data/yokatlas.db --ham data/ham/ozellik-2026-2026-09-25.jsonl.gz
|
||||||
|
*/
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import readline from "node:readline";
|
||||||
|
import zlib from "node:zlib";
|
||||||
|
import {
|
||||||
|
argOku,
|
||||||
|
calistir,
|
||||||
|
dbAc,
|
||||||
|
dbArgumani,
|
||||||
|
dur,
|
||||||
|
metaYaz,
|
||||||
|
yilArgumani,
|
||||||
|
type Db,
|
||||||
|
} from "./yokatlas-goc";
|
||||||
|
|
||||||
|
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||||
|
const PAGE_SIZE = 500;
|
||||||
|
const DELAY_MS = 500;
|
||||||
|
|
||||||
|
type HamKayit = Record<string, unknown>;
|
||||||
|
|
||||||
|
type ApiPage = {
|
||||||
|
content: HamKayit[];
|
||||||
|
totalPages: number;
|
||||||
|
totalElements: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function fetchPage(page: number): Promise<ApiPage> {
|
||||||
|
const res = await fetch(API, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
filters: {
|
||||||
|
puanTuru: null,
|
||||||
|
universiteId: [],
|
||||||
|
birimGrupId: [],
|
||||||
|
ilKodu: [],
|
||||||
|
birimTuruId: null,
|
||||||
|
universiteTuru: null,
|
||||||
|
bursOraniId: null,
|
||||||
|
ogrenimTuruId: null,
|
||||||
|
kilavuzKodu: null,
|
||||||
|
minBasariSirasi: null,
|
||||||
|
maxBasariSirasi: null,
|
||||||
|
},
|
||||||
|
page,
|
||||||
|
size: PAGE_SIZE,
|
||||||
|
sortBy: "kilavuzKodu",
|
||||||
|
direction: "ASC",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`API ${res.status} (sayfa ${page})`);
|
||||||
|
return (await res.json()) as ApiPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pozitif tam sayı; 0 / boş / sayı değil → null (API "veri yok"u 0 ile verir). */
|
||||||
|
function intOrNull(v: unknown): number | null {
|
||||||
|
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
|
||||||
|
return typeof n === "number" && Number.isFinite(n) && n > 0
|
||||||
|
? Math.round(n)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metinOrNull(v: unknown): string | null {
|
||||||
|
if (typeof v !== "string") return null;
|
||||||
|
const t = v.trim();
|
||||||
|
return t ? t : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Şema ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function ozellikTablolariniGarantiEt(db: Db): void {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS program_ozellik (
|
||||||
|
id TEXT PRIMARY KEY REFERENCES programs(id),
|
||||||
|
ucret INTEGER,
|
||||||
|
burs_orani TEXT,
|
||||||
|
ogrenim_dili TEXT,
|
||||||
|
ogrenim_turu TEXT,
|
||||||
|
akreditasyon TEXT,
|
||||||
|
uni_akreditasyon TEXT,
|
||||||
|
prof INTEGER,
|
||||||
|
doc INTEGER,
|
||||||
|
dou INTEGER,
|
||||||
|
argor INTEGER,
|
||||||
|
min_basari_sirasi INTEGER,
|
||||||
|
ilce TEXT,
|
||||||
|
kosul_kodlari TEXT,
|
||||||
|
yil INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS kosul (
|
||||||
|
kod TEXT PRIMARY KEY,
|
||||||
|
metin TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_program_ozellik_dili ON program_ozellik (ogrenim_dili);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_program_ozellik_turu ON program_ozellik (ogrenim_turu);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ham dosya ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function hamOku(dosya: string, yil: number): Promise<HamKayit[]> {
|
||||||
|
if (!fs.existsSync(dosya)) dur(`Ham dosya yok: ${dosya}`);
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: fs.createReadStream(dosya).pipe(zlib.createGunzip()),
|
||||||
|
});
|
||||||
|
const kayitlar: HamKayit[] = [];
|
||||||
|
let metaVar = false;
|
||||||
|
for await (const satir of rl) {
|
||||||
|
if (!satir) continue;
|
||||||
|
const obj = JSON.parse(satir) as HamKayit;
|
||||||
|
if (obj._meta) {
|
||||||
|
const meta = obj._meta as { yil: number };
|
||||||
|
if (meta.yil !== yil) dur(`Ham dosya ${meta.yil} yılı için, --yil ${yil} verildi.`);
|
||||||
|
metaVar = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
kayitlar.push(obj);
|
||||||
|
}
|
||||||
|
if (!metaVar) dur(`Ham dosyada _meta satırı yok: ${dosya}`);
|
||||||
|
return kayitlar;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apidenCek(yil: number): Promise<HamKayit[]> {
|
||||||
|
const ilk = await fetchPage(0);
|
||||||
|
const toplamSayfa = ilk.totalPages;
|
||||||
|
const kayitlar = [...ilk.content];
|
||||||
|
console.log(`API: ${ilk.totalElements} kayıt, ${toplamSayfa} sayfa`);
|
||||||
|
for (let p = 1; p < toplamSayfa; p++) {
|
||||||
|
await sleep(DELAY_MS);
|
||||||
|
const sayfa = await fetchPage(p);
|
||||||
|
kayitlar.push(...sayfa.content);
|
||||||
|
if (p % 10 === 0) console.log(` sayfa ${p}/${toplamSayfa}`);
|
||||||
|
}
|
||||||
|
const benzersiz = new Set(kayitlar.map((r) => String(r.kilavuzKodu))).size;
|
||||||
|
if (benzersiz !== ilk.totalElements) {
|
||||||
|
dur(`Benzersiz kod ${benzersiz} ≠ totalElements ${ilk.totalElements} (sayfalama kaydı)`);
|
||||||
|
}
|
||||||
|
const dizin = path.join(process.cwd(), "data", "ham");
|
||||||
|
fs.mkdirSync(dizin, { recursive: true });
|
||||||
|
const tarih = new Date().toISOString().slice(0, 10);
|
||||||
|
const dosya = path.join(dizin, `ozellik-${yil}-${tarih}.jsonl.gz`);
|
||||||
|
const gz = zlib.createGzip();
|
||||||
|
const out = fs.createWriteStream(dosya);
|
||||||
|
gz.pipe(out);
|
||||||
|
gz.write(JSON.stringify({ _meta: { yil, totalElements: ilk.totalElements, tarih, kaynak: "program-ozellik" } }) + "\n");
|
||||||
|
for (const r of kayitlar) gz.write(JSON.stringify(r) + "\n");
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
out.on("finish", () => res());
|
||||||
|
out.on("error", rej);
|
||||||
|
gz.end();
|
||||||
|
});
|
||||||
|
console.log(`Ham dosya: ${dosya}`);
|
||||||
|
return kayitlar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Yazım ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function kosulKodlari(r: HamKayit): { kodlar: string[]; metinler: Map<string, string> } {
|
||||||
|
const metinler = new Map<string, string>();
|
||||||
|
const liste = Array.isArray(r.kosulList) ? (r.kosulList as Record<string, string>[]) : [];
|
||||||
|
for (const k of liste) {
|
||||||
|
for (const [kod, metin] of Object.entries(k)) {
|
||||||
|
if (typeof metin === "string" && metin.trim()) metinler.set(kod, metin.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `kosul` alanı "155,18,21" biçiminde; listedeki kodlarla birleşir.
|
||||||
|
const hamKodlar = typeof r.kosul === "string" ? r.kosul.split(",") : [];
|
||||||
|
const kodlar = Array.from(
|
||||||
|
new Set([...hamKodlar.map((k) => k.trim()).filter(Boolean), ...metinler.keys()])
|
||||||
|
).sort((a, b) => Number(a) - Number(b));
|
||||||
|
return { kodlar, metinler };
|
||||||
|
}
|
||||||
|
|
||||||
|
calistir(async () => {
|
||||||
|
const yil = yilArgumani();
|
||||||
|
const dbYolu = dbArgumani();
|
||||||
|
const hamArg = argOku("ham");
|
||||||
|
|
||||||
|
const kayitlar = hamArg
|
||||||
|
? await hamOku(path.resolve(process.cwd(), hamArg), yil)
|
||||||
|
: await apidenCek(yil);
|
||||||
|
|
||||||
|
const db = dbAc(dbYolu);
|
||||||
|
ozellikTablolariniGarantiEt(db);
|
||||||
|
|
||||||
|
const mevcut = new Set(
|
||||||
|
(db.prepare("SELECT id FROM programs").all() as { id: string }[]).map((r) => r.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const yaz = db.prepare(`
|
||||||
|
INSERT INTO program_ozellik (id, ucret, burs_orani, ogrenim_dili, ogrenim_turu,
|
||||||
|
akreditasyon, uni_akreditasyon, prof, doc, dou, argor, min_basari_sirasi,
|
||||||
|
ilce, kosul_kodlari, yil)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
ucret = excluded.ucret, burs_orani = excluded.burs_orani,
|
||||||
|
ogrenim_dili = excluded.ogrenim_dili, ogrenim_turu = excluded.ogrenim_turu,
|
||||||
|
akreditasyon = excluded.akreditasyon, uni_akreditasyon = excluded.uni_akreditasyon,
|
||||||
|
prof = excluded.prof, doc = excluded.doc, dou = excluded.dou, argor = excluded.argor,
|
||||||
|
min_basari_sirasi = excluded.min_basari_sirasi, ilce = excluded.ilce,
|
||||||
|
kosul_kodlari = excluded.kosul_kodlari, yil = excluded.yil
|
||||||
|
`);
|
||||||
|
const kosulYaz = db.prepare(
|
||||||
|
"INSERT INTO kosul (kod, metin) VALUES (?, ?) ON CONFLICT(kod) DO UPDATE SET metin = excluded.metin"
|
||||||
|
);
|
||||||
|
|
||||||
|
let yazilan = 0;
|
||||||
|
let eslesmeyen = 0;
|
||||||
|
const tumKosullar = new Map<string, string>();
|
||||||
|
db.transaction(() => {
|
||||||
|
for (const r of kayitlar) {
|
||||||
|
const id = String(r.kilavuzKodu ?? "");
|
||||||
|
if (!mevcut.has(id)) {
|
||||||
|
eslesmeyen++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { kodlar, metinler } = kosulKodlari(r);
|
||||||
|
for (const [k, m] of metinler) tumKosullar.set(k, m);
|
||||||
|
yaz.run(
|
||||||
|
id,
|
||||||
|
intOrNull(r.ucret),
|
||||||
|
metinOrNull(r.bursOraniAdi),
|
||||||
|
metinOrNull(r.ogrenimDiliAdi),
|
||||||
|
metinOrNull(r.ogrenimTuruAdi),
|
||||||
|
metinOrNull(r.akreditasyon),
|
||||||
|
metinOrNull(r.uniAkreditasyon),
|
||||||
|
intOrNull(r.prof),
|
||||||
|
intOrNull(r.doc),
|
||||||
|
intOrNull(r.dou),
|
||||||
|
intOrNull(r.arGor),
|
||||||
|
intOrNull(r.minBasariSirasi),
|
||||||
|
metinOrNull(r.ilceAdi),
|
||||||
|
kodlar.length ? kodlar.join(",") : null,
|
||||||
|
yil
|
||||||
|
);
|
||||||
|
yazilan++;
|
||||||
|
}
|
||||||
|
for (const [k, m] of tumKosullar) kosulYaz.run(k, m);
|
||||||
|
metaYaz(db, "program_ozellik_yil", yil);
|
||||||
|
metaYaz(db, "program_ozellik_tarih", new Date().toISOString().slice(0, 10));
|
||||||
|
})();
|
||||||
|
|
||||||
|
const ozet = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT COUNT(*) AS n,
|
||||||
|
SUM(ucret IS NOT NULL) AS ucret, SUM(burs_orani IS NOT NULL) AS burs,
|
||||||
|
SUM(ogrenim_dili IS NOT NULL) AS dil, SUM(akreditasyon IS NOT NULL) AS akr,
|
||||||
|
SUM(prof IS NOT NULL) AS prof, SUM(min_basari_sirasi IS NOT NULL) AS baraj,
|
||||||
|
SUM(kosul_kodlari IS NOT NULL) AS kosul
|
||||||
|
FROM program_ozellik`
|
||||||
|
)
|
||||||
|
.get() as Record<string, number>;
|
||||||
|
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||||
|
db.close();
|
||||||
|
console.log(
|
||||||
|
`Yazıldı ${yazilan}, programs'ta olmayan ${eslesmeyen}, koşul metni ${tumKosullar.size}.\n` +
|
||||||
|
`Doluluk: ${JSON.stringify(ozet)}`
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -54,7 +54,10 @@ def sayfa(ad: str):
|
|||||||
|
|
||||||
|
|
||||||
# Öğrenci: sütun 1 ad, 4 öğrenim türü, 19 genel toplam (T).
|
# Öğrenci: sütun 1 ad, 4 öğrenim türü, 19 genel toplam (T).
|
||||||
|
# Kadın/erkek: ön lisans E/K sütun 5/6, lisans E/K sütun 8/9 — yalnız örgün
|
||||||
|
# ön lisans + lisans (tercih yapan öğrencinin gireceği düzey).
|
||||||
ogrenci: dict[str, int] = {}
|
ogrenci: dict[str, int] = {}
|
||||||
|
cinsiyet: dict[str, list[int]] = {}
|
||||||
s = sayfa("ogrenci")
|
s = sayfa("ogrenci")
|
||||||
for r in range(4, s.nrows):
|
for r in range(4, s.nrows):
|
||||||
v = s.row_values(r)
|
v = s.row_values(r)
|
||||||
@@ -64,6 +67,9 @@ for r in range(4, s.nrows):
|
|||||||
if tur in ("BİRİNCİ Ö.", "İKİNCİ Ö."):
|
if tur in ("BİRİNCİ Ö.", "İKİNCİ Ö."):
|
||||||
k = sayfa_slug(ad)
|
k = sayfa_slug(ad)
|
||||||
ogrenci[k] = ogrenci.get(k, 0) + tamsayi(v[19])
|
ogrenci[k] = ogrenci.get(k, 0) + tamsayi(v[19])
|
||||||
|
c = cinsiyet.setdefault(k, [0, 0])
|
||||||
|
c[0] += tamsayi(v[6]) + tamsayi(v[9]) # kadın
|
||||||
|
c[1] += tamsayi(v[5]) + tamsayi(v[8]) # erkek
|
||||||
|
|
||||||
# Akademisyen: T sütunları Prof 6, Doç 9, Dr. Öğr. Üyesi 12, genel toplam 21.
|
# Akademisyen: T sütunları Prof 6, Doç 9, Dr. Öğr. Üyesi 12, genel toplam 21.
|
||||||
akademik: dict[str, tuple[int, int]] = {}
|
akademik: dict[str, tuple[int, int]] = {}
|
||||||
@@ -86,7 +92,11 @@ for k in sorted(sayfalar):
|
|||||||
uye, toplam = akademik[k]
|
uye, toplam = akademik[k]
|
||||||
if ogrenci[k] == 0 or uye == 0:
|
if ogrenci[k] == 0 or uye == 0:
|
||||||
continue
|
continue
|
||||||
satirlar.append(f' "{k}": {{ ogrenci: {ogrenci[k]}, ogretimUyesi: {uye}, akademisyen: {toplam} }},')
|
kadin, erkek = cinsiyet.get(k, (0, 0))
|
||||||
|
satirlar.append(
|
||||||
|
f' "{k}": {{ ogrenci: {ogrenci[k]}, ogretimUyesi: {uye}, akademisyen: {toplam}, '
|
||||||
|
f'kadin: {kadin}, erkek: {erkek} }},'
|
||||||
|
)
|
||||||
|
|
||||||
oranlar = sorted(ogrenci[k] / akademik[k][0] for k in sayfalar
|
oranlar = sorted(ogrenci[k] / akademik[k][0] for k in sayfalar
|
||||||
if ogrenci.get(k) and akademik.get(k, (0, 0))[0])
|
if ogrenci.get(k) and akademik.get(k, (0, 0))[0])
|
||||||
@@ -106,6 +116,7 @@ cikti = f'''// YÖK İstatistik {DONEM}: üniversite başına örgün öğrenci
|
|||||||
//
|
//
|
||||||
// Öğrenci = örgün (birinci + ikinci öğretim), tüm düzeyler; uzaktan ve açık
|
// Öğrenci = örgün (birinci + ikinci öğretim), tüm düzeyler; uzaktan ve açık
|
||||||
// öğretim dahil değil. Öğretim üyesi = Prof. + Doç. + Dr. Öğr. Üyesi.
|
// öğretim dahil değil. Öğretim üyesi = Prof. + Doç. + Dr. Öğr. Üyesi.
|
||||||
|
// Kadın/erkek = örgün ön lisans + lisans öğrencisi (E/K sütunları).
|
||||||
// Anahtar: /universite/[slug] sayfa slug'ı. Burada olmayan üniversitenin
|
// Anahtar: /universite/[slug] sayfa slug'ı. Burada olmayan üniversitenin
|
||||||
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
||||||
|
|
||||||
@@ -123,6 +134,10 @@ export type UniIstatistik = {{
|
|||||||
ogretimUyesi: number;
|
ogretimUyesi: number;
|
||||||
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
||||||
akademisyen: number;
|
akademisyen: number;
|
||||||
|
/** Örgün ön lisans + lisans kadın öğrenci */
|
||||||
|
kadin: number;
|
||||||
|
/** Örgün ön lisans + lisans erkek öğrenci */
|
||||||
|
erkek: number;
|
||||||
}};
|
}};
|
||||||
|
|
||||||
export const YOK_ISTATISTIK: Record<string, UniIstatistik> = {{
|
export const YOK_ISTATISTIK: Record<string, UniIstatistik> = {{
|
||||||
@@ -138,5 +153,16 @@ export function uniIstatistikBul(slug: string): UniIstatistik | null {{
|
|||||||
export function ogrenciBasinaUye(i: UniIstatistik): number {{
|
export function ogrenciBasinaUye(i: UniIstatistik): number {{
|
||||||
return Math.round(i.ogrenci / i.ogretimUyesi);
|
return Math.round(i.ogrenci / i.ogretimUyesi);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Örgün ön lisans + lisans öğrencisinde kadın yüzdesi (0-100, tam sayı);
|
||||||
|
* iki sayı da sıfırsa null. Üniversite düzeyidir, bölüm düzeyinde veri yok —
|
||||||
|
* bilgi olarak gösterilir, filtre ya da öncelik yapılmaz.
|
||||||
|
*/
|
||||||
|
export function kadinYuzdesi(i: UniIstatistik): number | null {{
|
||||||
|
const toplam = i.kadin + i.erkek;
|
||||||
|
if (toplam === 0) return null;
|
||||||
|
return Math.round((i.kadin / toplam) * 100);
|
||||||
|
}}
|
||||||
'''
|
'''
|
||||||
(KOK / "src" / "lib" / "yok-istatistik.ts").write_text(cikti, encoding="utf-8")
|
(KOK / "src" / "lib" / "yok-istatistik.ts").write_text(cikti, encoding="utf-8")
|
||||||
|
|||||||
29
src/app/api/soru/profil/route.ts
Normal file
29
src/app/api/soru/profil/route.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
import { getSession } from "@/lib/session";
|
||||||
|
import { profilAlanYaz, profilGetir } from "@/lib/danisman-profil-db";
|
||||||
|
import { PROFIL_ALANLARI, profilSatirlari, type ProfilAlani } from "@/lib/danisman-profil";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Danışman profili — sohbetten `profil_guncelle` aracıyla yazılan alanlar.
|
||||||
|
* GET: alanlar + öğrenciye okunan satırlar. DELETE {alan}: tek alanı siler
|
||||||
|
* (KVKK: öğrenci her alanı görür ve tek tıkla siler). Yazma yalnız araç
|
||||||
|
* üzerinden; elle düzenleme sohbette "şunu değiştir" diyerek yapılır.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session) return Response.json({ error: "Oturum gerekli." }, { status: 401 });
|
||||||
|
const profil = await profilGetir(session.user.id);
|
||||||
|
return Response.json({ profil, satirlar: profilSatirlari(profil) });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session) return Response.json({ error: "Oturum gerekli." }, { status: 401 });
|
||||||
|
const body = (await request.json().catch(() => null)) as { alan?: unknown } | null;
|
||||||
|
const alan = typeof body?.alan === "string" ? (body.alan as ProfilAlani) : null;
|
||||||
|
if (!alan || !PROFIL_ALANLARI.includes(alan)) {
|
||||||
|
return Response.json({ error: "Geçersiz alan." }, { status: 400 });
|
||||||
|
}
|
||||||
|
const profil = await profilAlanYaz(session.user.id, alan, null);
|
||||||
|
return Response.json({ profil, satirlar: profilSatirlari(profil) });
|
||||||
|
}
|
||||||
@@ -4,12 +4,16 @@ import { getSession } from "@/lib/session";
|
|||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import {
|
import {
|
||||||
spendCreditForMessage,
|
spendCreditForMessage,
|
||||||
|
bedelsizMesajKaydet,
|
||||||
grantCredits,
|
grantCredits,
|
||||||
krediBittiIsaretle,
|
krediBittiIsaretle,
|
||||||
} from "@/lib/credits";
|
} from "@/lib/credits";
|
||||||
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||||
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { sohbetAkisi } from "@/lib/ai/cagri";
|
import { sohbetAkisiAraclarla } from "@/lib/ai/cagri";
|
||||||
|
import { ARACLAR, aracCalistirici, type AracBaglami } from "@/lib/ai/araclar";
|
||||||
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
|
import { profilSatirlari, type DanismanProfili } from "@/lib/danisman-profil";
|
||||||
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
import type { RaporParams } from "@/features/rapor/types/rapor";
|
import type { RaporParams } from "@/features/rapor/types/rapor";
|
||||||
@@ -18,12 +22,36 @@ import { PUAN_TURLERI } from "@/types/yokatlas";
|
|||||||
import { ACIK_SATIR } from "@/lib/rapor-maske";
|
import { ACIK_SATIR } from "@/lib/rapor-maske";
|
||||||
|
|
||||||
const MAX_GECMIS = 12;
|
const MAX_GECMIS = 12;
|
||||||
|
/**
|
||||||
|
* Profil kaydı öncesi "kaydedeyim mi?" onayı (KVKK Üretken YZ Rehberi:
|
||||||
|
* kişiselleştirme ayrı işleme faaliyeti; onaysız otomatik kayıt avukata
|
||||||
|
* sorulacak — docs/arastirma/2026-09-25-kisisellestirme/hukuk-ve-rakipler.md
|
||||||
|
* C.3/10). Bedeli: "evet" cevabı bir kredi. Kapatılırsa model doğrudan kaydeder.
|
||||||
|
*/
|
||||||
|
const PROFIL_ONAY_SOR = true;
|
||||||
|
|
||||||
|
/** Danışmanın onay sorusunda geçen sabit ibare; istemci de bunu görmez, yalnız sunucu arar. */
|
||||||
|
const ONAY_SORUSU = "profiline kaydedeyim mi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onay mesajı mı: son danışman mesajı "kaydedeyim mi?" diye sormuş VE aday
|
||||||
|
* kısa bir evet/hayır yazmış. Bu mesaj kredi harcamaz (Bilal, 26 Eyl 2026).
|
||||||
|
* Kısa tutulur ki "evet ama İzmir'de hukuk kaç sırayla alıyor" gibi gerçek
|
||||||
|
* sorular bedava geçmesin.
|
||||||
|
*/
|
||||||
|
function onayMesajiMi(mesaj: string, sonAsistan: string | undefined): boolean {
|
||||||
|
if (!sonAsistan || !sonAsistan.toLocaleLowerCase("tr-TR").includes(ONAY_SORUSU)) return false;
|
||||||
|
const m = mesaj.trim().toLocaleLowerCase("tr-TR");
|
||||||
|
if (m.length > 40) return false;
|
||||||
|
return /^(evet|olur|tamam|kaydet|kaydedebilirsin|kaydedelim|hayır|hayir|kaydetme|gerek yok|istemiyorum|şimdilik hayır)\b/.test(m);
|
||||||
|
}
|
||||||
const MAX_MESAJ_UZUNLUK = 2000;
|
const MAX_MESAJ_UZUNLUK = 2000;
|
||||||
|
|
||||||
function sistemPromptu(opts: {
|
function sistemPromptu(opts: {
|
||||||
params: RaporParams;
|
params: RaporParams;
|
||||||
listeOzeti: string;
|
listeOzeti: string;
|
||||||
kilitli?: boolean;
|
kilitli?: boolean;
|
||||||
|
profil?: DanismanProfili;
|
||||||
}): string {
|
}): string {
|
||||||
const { params } = opts;
|
const { params } = opts;
|
||||||
const secimVar =
|
const secimVar =
|
||||||
@@ -50,7 +78,28 @@ KURALLAR:
|
|||||||
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
|
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
|
||||||
- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma —
|
- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma —
|
||||||
güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş.
|
güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş.
|
||||||
- Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle.
|
- Bilmediğin güncel veriyi uydurma: taban sırası, kontenjan, ücret, burs,
|
||||||
|
dil, akreditasyon, koşul ya da üniversite künyesi gerekince ARAÇLARI çağır
|
||||||
|
(program_ara, program_detay, universite_profili); araç veremiyorsa
|
||||||
|
"elimde veri yok" de ve YÖK Atlas'a bakmasını söyle.
|
||||||
|
- ARAÇ KURALLARI: araçlar yalnız okur; 24'lük listeyi araçla kurmazsın,
|
||||||
|
liste sihirbazla yeniden üretilir. Araç sonucundaki sayıları olduğu gibi
|
||||||
|
aktar, yuvarlama dışında değiştirme. Bir soruda en fazla 2-3 araç çağır.
|
||||||
|
- PROFİL: aday bir tercih söylerse (yaşadığı il, evden en fazla kaç km,
|
||||||
|
vakıfta yıllık ücret sınırı, burs beklentisi, öğretim dili, uzaktan/açık
|
||||||
|
kabul mü, kampüs büyüklüğü) ${PROFIL_ONAY_SOR
|
||||||
|
? `HEMEN KAYDETME: cevabının sonunda tek cümleyle, tam bu ibareyle sor:
|
||||||
|
"Bunu profiline kaydedeyim mi? (Evet demek kredi harcamaz; listen ve
|
||||||
|
aramalar buna göre süzülür, istediğin zaman silebilirsin.)" Aday
|
||||||
|
evet/kaydet derse o zaman profil_guncelle ile kaydet ve "Kaydettim: …" de.
|
||||||
|
Aday açıkça "kaydet" diye yazdıysa sormadan kaydet.`
|
||||||
|
: `profil_guncelle ile kaydet ve cevabının sonunda tek cümleyle söyle:
|
||||||
|
"Şunu kaydettim: …". Emin değilsen önce sor, tahminle kaydetme.`}
|
||||||
|
Bunların DIŞINDA hiçbir şeyi kaydetme ve sorma:
|
||||||
|
sağlık/engel, din, aile durumu, gelir, cinsiyet, siyasi görüş. Aday
|
||||||
|
bunları kendiliğinden yazsa bile profile YAZMA; yalnız cevabında dikkate al.
|
||||||
|
- Kadın/erkek öğrenci oranı üniversite genelidir (bölüm bazında veri yok);
|
||||||
|
sorulursa bilgi olarak söyle, filtre ya da tavsiye ölçütü yapma.
|
||||||
- Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat.
|
- Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat.
|
||||||
- Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır.
|
- Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır.
|
||||||
- GÜVENLİK, öteki kuralların önünde gelir: aday umutsuzluk, kendine zarar
|
- GÜVENLİK, öteki kuralların önünde gelir: aday umutsuzluk, kendine zarar
|
||||||
@@ -77,6 +126,13 @@ Tercih Dönemi Paketi'yle (299 TL, /paket) açıldığını söyle; açık satı
|
|||||||
tercih soruları üzerinden yardımcı olmaya devam et.`,
|
tercih soruları üzerinden yardımcı olmaya devam et.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const profilSatir = opts.profil ? profilSatirlari(opts.profil) : [];
|
||||||
|
if (profilSatir.length > 0) {
|
||||||
|
parcalar.push(
|
||||||
|
`ADAYIN SOHBETTE SÖYLEDİĞİ VE KAYDEDİLEN TERCİHLERİ (profil; program_ara bunları otomatik uygular):\n` +
|
||||||
|
profilSatir.map((s) => `- ${s.metin}`).join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (secimVar) {
|
if (secimVar) {
|
||||||
parcalar.push(
|
parcalar.push(
|
||||||
`ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({
|
`ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({
|
||||||
@@ -156,7 +212,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// Üç okuma da yalnızca session.user.id'ye bağlı — tek round trip'te paralel.
|
// Üç okuma da yalnızca session.user.id'ye bağlı — tek round trip'te paralel.
|
||||||
// Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın;
|
// Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın;
|
||||||
// hasPaket'i spendCreditForMessage değiştirmez, erken okumak güvenli.
|
// hasPaket'i spendCreditForMessage değiştirmez, erken okumak güvenli.
|
||||||
const [rapor, gecmisHam, kullanici] = await Promise.all([
|
const [rapor, gecmisHam, kullanici, profil] = await Promise.all([
|
||||||
// Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz)
|
// Soru sormak için önce liste oluşturulmuş olmalı (bayat dev mock sayılmaz)
|
||||||
kullanicininRaporu(session.user.id),
|
kullanicininRaporu(session.user.id),
|
||||||
appDb
|
appDb
|
||||||
@@ -169,6 +225,7 @@ export async function POST(request: NextRequest) {
|
|||||||
where: eq(schema.user.id, session.user.id),
|
where: eq(schema.user.id, session.user.id),
|
||||||
columns: { hasPaket: true },
|
columns: { hasPaket: true },
|
||||||
}),
|
}),
|
||||||
|
profilGetir(session.user.id),
|
||||||
]);
|
]);
|
||||||
if (!rapor?.result) {
|
if (!rapor?.result) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
@@ -178,12 +235,18 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
const gecmis = gecmisHam.reverse();
|
const gecmis = gecmisHam.reverse();
|
||||||
|
|
||||||
|
// Profil onayı ("kaydedeyim mi?" → "evet") kredi harcamaz; iade de yok.
|
||||||
|
const sonAsistan = [...gecmis].reverse().find((m) => m.role === "assistant")?.content;
|
||||||
|
const bedelsiz = onayMesajiMi(mesaj, sonAsistan);
|
||||||
|
|
||||||
// Kredi düşümü stream'den önce — yetersizse hiç başlama
|
// Kredi düşümü stream'den önce — yetersizse hiç başlama
|
||||||
const harcama = await spendCreditForMessage({
|
const harcama = bedelsiz
|
||||||
userId: session.user.id,
|
? await bedelsizMesajKaydet({ userId: session.user.id, content: mesaj, clientMessageId })
|
||||||
content: mesaj,
|
: await spendCreditForMessage({
|
||||||
clientMessageId,
|
userId: session.user.id,
|
||||||
});
|
content: mesaj,
|
||||||
|
clientMessageId,
|
||||||
|
});
|
||||||
if (!harcama.ok) {
|
if (!harcama.ok) {
|
||||||
if (harcama.error === "INSUFFICIENT") {
|
if (harcama.error === "INSUFFICIENT") {
|
||||||
await krediBittiIsaretle(session.user.id);
|
await krediBittiIsaretle(session.user.id);
|
||||||
@@ -213,11 +276,21 @@ export async function POST(request: NextRequest) {
|
|||||||
].join("\n")
|
].join("\n")
|
||||||
: tamOzet;
|
: tamOzet;
|
||||||
|
|
||||||
|
const raporParams = rapor.params as RaporParams;
|
||||||
const system = sistemPromptu({
|
const system = sistemPromptu({
|
||||||
params: rapor.params as RaporParams,
|
params: raporParams,
|
||||||
listeOzeti,
|
listeOzeti,
|
||||||
kilitli,
|
kilitli,
|
||||||
|
profil,
|
||||||
});
|
});
|
||||||
|
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci) ve profil
|
||||||
|
// (profil_guncelle yazınca aynı tur içinde tazelenir).
|
||||||
|
const aracBaglami: AracBaglami = {
|
||||||
|
userId: session.user.id,
|
||||||
|
sira: raporParams.sira,
|
||||||
|
tur: raporParams.tur,
|
||||||
|
profil,
|
||||||
|
};
|
||||||
|
|
||||||
// Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla)
|
// Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla)
|
||||||
if (process.env.NODE_ENV !== "production" && !anahtarVar()) {
|
if (process.env.NODE_ENV !== "production" && !anahtarVar()) {
|
||||||
@@ -253,12 +326,14 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!anahtarVar()) {
|
if (!anahtarVar()) {
|
||||||
await grantCredits({
|
if (!bedelsiz) {
|
||||||
userId: session.user.id,
|
await grantCredits({
|
||||||
delta: 1,
|
userId: session.user.id,
|
||||||
reason: "refund",
|
delta: 1,
|
||||||
refId: kullaniciMesajId,
|
reason: "refund",
|
||||||
});
|
refId: kullaniciMesajId,
|
||||||
|
});
|
||||||
|
}
|
||||||
console.error("[soru] Yapay Zeka anahtarı tanımlı değil");
|
console.error("[soru] Yapay Zeka anahtarı tanımlı değil");
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
@@ -286,10 +361,13 @@ export async function POST(request: NextRequest) {
|
|||||||
let metin = "";
|
let metin = "";
|
||||||
try {
|
try {
|
||||||
// Sağlayıcıya bağlanma hatası da ilk parçada burada yakalanır
|
// Sağlayıcıya bağlanma hatası da ilk parçada burada yakalanır
|
||||||
for await (const parca of sohbetAkisi({
|
for await (const parca of sohbetAkisiAraclarla({
|
||||||
system,
|
system,
|
||||||
mesajlar,
|
mesajlar,
|
||||||
maxTokens: 1024,
|
maxTokens: 1024,
|
||||||
|
araclar: ARACLAR,
|
||||||
|
calistir: aracCalistirici(aracBaglami),
|
||||||
|
onArac: (ad) => console.info("[soru] araç:", ad),
|
||||||
})) {
|
})) {
|
||||||
metin += parca;
|
metin += parca;
|
||||||
controller.enqueue(encoder.encode(parca));
|
controller.enqueue(encoder.encode(parca));
|
||||||
@@ -319,12 +397,14 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
await grantCredits({
|
if (!bedelsiz) {
|
||||||
userId,
|
await grantCredits({
|
||||||
delta: 1,
|
userId,
|
||||||
reason: "refund",
|
delta: 1,
|
||||||
refId: kullaniciMesajId,
|
reason: "refund",
|
||||||
}).catch(() => {});
|
refId: kullaniciMesajId,
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.error("[soru] stream hatası:", err);
|
console.error("[soru] stream hatası:", err);
|
||||||
controller.error(err);
|
controller.error(err);
|
||||||
@@ -337,6 +417,8 @@ export async function POST(request: NextRequest) {
|
|||||||
"Content-Type": "text/plain; charset=utf-8",
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
"X-Content-Type-Options": "nosniff",
|
"X-Content-Type-Options": "nosniff",
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
|
// İstemci bakiyeyi buna göre düşürür (onay mesajı bedelsiz)
|
||||||
|
"X-Kredi-Dusuldu": bedelsiz ? "0" : "1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,16 @@ export const metadata: Metadata = sayfaMetadata({
|
|||||||
path: "/gizlilik",
|
path: "/gizlilik",
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOT: MVP taslağıdır; yayına almadan önce hukuki gözden geçirme gerekir.
|
// NOT: 25 Eyl 2026 taslağı — v2 dalı. Yayına almadan önce hukuki gözden
|
||||||
|
// geçirme gerekir (docs/arastirma/2026-09-25-kisisellestirme/hukuk-ve-rakipler.md
|
||||||
|
// C.2). Kodun gerçekte yaptığıyla birebir örtüşmesi zorunludur: sağlayıcı,
|
||||||
|
// alanlar ya da süreler değişirse bu metin aynı commit'te güncellenir.
|
||||||
|
// TODO(bilal): veri sorumlusunun ticari unvanı ve adresi (Tebliğ md. 5/1-a).
|
||||||
|
// TODO(avukat): yurt dışı aktarımın dayanağı (standart sözleşme / Türkiye'de
|
||||||
|
// model) kesinleşince "Hizmet sağlayıcılar" bölümüne yazılacak; saklama
|
||||||
|
// süreleri onaylanacak.
|
||||||
export default function GizlilikPage() {
|
export default function GizlilikPage() {
|
||||||
|
const baslik = "font-heading text-lg font-bold text-slate-900";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
<main className="mx-auto w-full max-w-2xl px-4 py-16">
|
||||||
@@ -22,7 +30,7 @@ export default function GizlilikPage() {
|
|||||||
Gizlilik ve Kişisel Verilerin Korunması
|
Gizlilik ve Kişisel Verilerin Korunması
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-sm text-slate-500">
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
Son güncelleme: 2 Ağustos 2026
|
Son güncelleme: 25 Eylül 2026
|
||||||
</p>
|
</p>
|
||||||
</Parallax>
|
</Parallax>
|
||||||
|
|
||||||
@@ -33,66 +41,187 @@ export default function GizlilikPage() {
|
|||||||
strength={4}
|
strength={4}
|
||||||
className="mt-8 space-y-6 text-sm leading-7 text-slate-700"
|
className="mt-8 space-y-6 text-sm leading-7 text-slate-700"
|
||||||
>
|
>
|
||||||
<section>
|
<section className="rounded-2xl border border-slate-200 bg-slate-50 p-5">
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
<h2 className={baslik}>Kısa özet</h2>
|
||||||
Hangi verileri işliyoruz?
|
<ul className="mt-2 list-disc space-y-1 pl-5">
|
||||||
</h2>
|
<li>
|
||||||
<p className="mt-2">
|
Senden yalnız e-posta, başarı sıralaman, sihirbaz seçimlerin,
|
||||||
KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS
|
danışmana yazdıkların ve danışmana söyleyip kaydettiğin
|
||||||
başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni
|
tercihler alınır. Ad, kimlik numarası, adres, cinsiyet, sağlık
|
||||||
üretmek için), danışman sohbetine yazdığın mesajları (sorularını
|
ya da gelir bilgisi istenmez.
|
||||||
yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler.
|
</li>
|
||||||
Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında
|
<li>
|
||||||
gerçekleşir.
|
Bunlar yalnız tercih listeni üretmek ve sorularını yanıtlamak
|
||||||
</p>
|
için kullanılır; satılmaz, reklam için kullanılmaz.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Danışmana yazdıkların yanıt üretilmesi için yurt dışındaki
|
||||||
|
yapay zekâ sağlayıcısına gider; adın ve e-postan gitmez.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Kaydedilen tercihlerini listem sayfasında görür, tek tıkla
|
||||||
|
silersin. Hesabını sildirmek için yazman yeterli.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
18 yaşından küçüksen bu metni velinle birlikte oku.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
<h2 className={baslik}>Veri sorumlusu</h2>
|
||||||
Ne için kullanıyoruz?
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi
|
Bu sitedeki kişisel verilerin veri sorumlusu KolayTercih’tir
|
||||||
bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş
|
[şirket unvanı ve adresi eklenecek]. Sorularını ve başvurularını{" "}
|
||||||
bağlantısı, ödeme bilgilendirmesi) göndermek için kullanılır.
|
|
||||||
Verilerini üçüncü taraflara satmayız, reklam amacıyla paylaşmayız.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
|
||||||
Hizmet sağlayıcılar ve yurt dışına aktarım
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2">
|
|
||||||
Hizmeti çalıştırabilmek için birkaç dış sağlayıcıdan yararlanıyoruz
|
|
||||||
ve verinin bir kısmı yalnızca ilgili iş için bu sağlayıcılara
|
|
||||||
iletilir. Giriş bağlantısı ve bilgilendirme e-postaları Resend
|
|
||||||
üzerinden gönderilir; e-posta adresin bu amaçla Avrupa
|
|
||||||
Birliği’ndeki (İrlanda) sunucularda işlenir. Ödemeler,
|
|
||||||
Türkiye’de yerleşik iyzico altyapısında gerçekleşir. Danışman
|
|
||||||
sohbetine yazdığın mesajlar, yanıt üretilebilmesi için
|
|
||||||
Anthropic’in yapay zekâ servisine (ABD) iletilir; bu mesajlar
|
|
||||||
sağlayıcının yapay zekâ modellerini eğitmek için kullanılmaz.
|
|
||||||
KVKK açısından bunlardan e-posta ve yapay zekâ sağlayıcısına yapılan
|
|
||||||
iletim yurt dışına aktarım sayılır ve hizmetin sunulabilmesi için
|
|
||||||
zorunludur.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
|
||||||
Saklama ve silme
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2">
|
|
||||||
Hesabını sildirmek veya verilerinin bir kopyasını istemek için{" "}
|
|
||||||
<Link href="/iletisim" className="text-primary underline">
|
<Link href="/iletisim" className="text-primary underline">
|
||||||
iletişim sayfasındaki
|
iletişim sayfasındaki
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
adresten bize yazabilirsin; talepler 30 gün içinde
|
e-posta adresine iletebilirsin.
|
||||||
sonuçlandırılır.
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
<h2 className={baslik}>Hangi verileri, ne için, hangi dayanakla işliyoruz?</h2>
|
||||||
Çerezler ve analitik
|
<ul className="mt-2 list-disc space-y-2 pl-5">
|
||||||
</h2>
|
<li>
|
||||||
|
<strong>E-posta adresin</strong> — hesap oluşturmak, giriş
|
||||||
|
bağlantısı ve ödeme bilgilendirmesi göndermek için.
|
||||||
|
Dayanak: seninle kurulan hizmet sözleşmesinin kurulması ve
|
||||||
|
ifası.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>YKS başarı sıralaman ve sihirbaz seçimlerin</strong>{" "}
|
||||||
|
(ilgi alanı, il, devlet/vakıf, öncelikler) — 24’lük tercih
|
||||||
|
listeni üretmek için. Dayanak: sözleşmenin ifası.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Danışman sohbetine yazdığın mesajlar</strong> —
|
||||||
|
sorularını listen bağlamında yanıtlamak için. Dayanak:
|
||||||
|
sözleşmenin ifası. Sohbete ad, telefon, kimlik numarası,
|
||||||
|
sağlık ya da aile bilgisi yazma; danışmanın bunlara ihtiyacı
|
||||||
|
yok ve bunları saklamak istemiyoruz.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Danışman profilin</strong> — sohbette söylediğin ve{" "}
|
||||||
|
<em>“kaydedeyim mi?”</em> sorusuna evet dediğin
|
||||||
|
tercihler: yaşadığın il, evden en fazla kaç km, vakıfta yıllık
|
||||||
|
ücret üst sınırı, burs beklentin, öğretim dili, uzaktan/açık
|
||||||
|
öğretim kabulün, kampüs büyüklüğü tercihin. Bu alanlar sabit
|
||||||
|
bir listedir; serbest metin kaydedilmez, sağlık, din, gelir ya
|
||||||
|
da cinsiyet gibi bilgiler bu profile hiçbir zaman yazılmaz.
|
||||||
|
Profil yalnız senin listeni ve danışman aramalarını süzmek için
|
||||||
|
kullanılır; reklam, pazarlama ya da e-posta için kullanılmaz.
|
||||||
|
Profil boşken de hizmet tam çalışır. Listem sayfasındaki
|
||||||
|
“Kaydettiklerim” çiplerinden her alanı görür ve tek
|
||||||
|
tıkla silersin. Dayanak: senin talimatınla sözleşmenin ifası.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sipariş kayıtların</strong> (paket alırsan) — fatura
|
||||||
|
ve muhasebe yükümlülüğü için. Kart bilgilerin bize hiç ulaşmaz;
|
||||||
|
ödeme iyzico altyapısında gerçekleşir. Dayanak: hukuki
|
||||||
|
yükümlülük.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Kullanım ölçümü</strong> — hangi sayfaların
|
||||||
|
görüntülendiği ve hangi adımların tamamlandığı (aşağıda
|
||||||
|
“Çerezler ve analitik”). Dayanak: siteyi
|
||||||
|
geliştirmeye yönelik meşru menfaatimiz.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2">
|
||||||
|
Verilerini üçüncü taraflara satmayız, reklam amacıyla
|
||||||
|
paylaşmayız. Program ve üniversite bilgileri (taban sıralamaları,
|
||||||
|
ücretler, öğrenci sayıları, kadın/erkek oranı) kişisel veri
|
||||||
|
değildir; YÖK Atlas ve YÖK İstatistik’ten alınır ve
|
||||||
|
sayfalarda kaynağıyla gösterilir.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className={baslik}>Hizmet sağlayıcılar ve yurt dışına aktarım</h2>
|
||||||
|
<p className="mt-2">
|
||||||
|
Hizmeti çalıştırabilmek için birkaç dış sağlayıcıdan yararlanıyoruz
|
||||||
|
ve verinin yalnız ilgili iş için gereken kısmı bu sağlayıcılara
|
||||||
|
iletilir:
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2 list-disc space-y-2 pl-5">
|
||||||
|
<li>
|
||||||
|
<strong>Resend (İrlanda)</strong> — giriş bağlantısı ve
|
||||||
|
bilgilendirme e-postaları; e-posta adresin bu amaçla Avrupa
|
||||||
|
Birliği’ndeki sunucularda işlenir.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>iyzico (Türkiye)</strong> — ödeme.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Yapay zekâ sağlayıcısı</strong> — danışman sohbetine
|
||||||
|
yazdığın mesajlar, listendeki program bilgileri ve kaydettiğin
|
||||||
|
tercihler, yanıt üretilebilmesi için OpenRouter (ABD) aracılığıyla
|
||||||
|
kullandığımız dil modelinin sağlayıcısına (bugün DeepSeek)
|
||||||
|
iletilir. Adın ve e-postan modele gönderilmez. Tercih listeni
|
||||||
|
üreten yapay zekâ da aynı yoldan çalışır; ona sıralaman,
|
||||||
|
seçimlerin ve aday program havuzu gider.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2">
|
||||||
|
KVKK açısından e-posta ve yapay zekâ sağlayıcısına yapılan iletim
|
||||||
|
yurt dışına aktarımdır ve hizmetin sunulabilmesi için gereklidir.
|
||||||
|
Sağlayıcı değişirse bu bölüm aynı gün güncellenir.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className={baslik}>Saklama ve silme</h2>
|
||||||
|
<ul className="mt-2 list-disc space-y-1 pl-5">
|
||||||
|
<li>
|
||||||
|
Sohbet mesajların ve danışman profilin: hesabın açık kaldığı
|
||||||
|
sürece; her ikisini de listem sayfasından kendin silebilirsin.
|
||||||
|
Tercih döneminin bitiminden 12 ay sonra sohbet mesajları ve
|
||||||
|
profil silinir.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Hesabını sildirme ya da verilerinin kopyasını isteme talebin
|
||||||
|
30 gün içinde sonuçlandırılır; işlenme sebebi ortadan kalkan
|
||||||
|
veri en geç 3 ay içinde silinir.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Sipariş ve fatura kayıtları vergi mevzuatı gereği 10 yıl
|
||||||
|
saklanır.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className={baslik}>Hakların</h2>
|
||||||
|
<p className="mt-2">
|
||||||
|
KVKK’nın 11. maddesi gereği bize başvurarak şunları
|
||||||
|
yapabilirsin: verilerinin işlenip işlenmediğini öğrenmek,
|
||||||
|
işlendiyse bilgi istemek, amacına uygun kullanılıp
|
||||||
|
kullanılmadığını öğrenmek, aktarıldığı üçüncü kişileri bilmek,
|
||||||
|
eksik ya da yanlış işlenmişse düzeltilmesini istemek,
|
||||||
|
silinmesini ya da yok edilmesini istemek, bu işlemlerin
|
||||||
|
aktarıldığı üçüncü kişilere bildirilmesini istemek, yalnızca
|
||||||
|
otomatik sistemlerle analiz edilmesi sonucu aleyhine bir sonuç
|
||||||
|
çıkmasına itiraz etmek ve kanuna aykırı işleme yüzünden zarara
|
||||||
|
uğradıysan tazminat istemek. Başvurunu{" "}
|
||||||
|
<Link href="/iletisim" className="text-primary underline">
|
||||||
|
iletişim sayfasındaki
|
||||||
|
</Link>{" "}
|
||||||
|
e-posta adresine yaz; en geç 30 gün içinde yanıtlarız.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className={baslik}>18 yaşından küçüksen</h2>
|
||||||
|
<p className="mt-2">
|
||||||
|
Bu metni velinle birlikte oku. Paket satın alınacaksa ödemeyi
|
||||||
|
velin yapmalı. Verilerin hiçbir şekilde pazarlama için
|
||||||
|
kullanılmaz.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className={baslik}>Çerezler ve analitik</h2>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır;
|
Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır;
|
||||||
reklam veya izleme çerezi kullanılmaz. Siteyi geliştirebilmek için
|
reklam veya izleme çerezi kullanılmaz. Siteyi geliştirebilmek için
|
||||||
@@ -105,7 +234,8 @@ export default function GizlilikPage() {
|
|||||||
yalnızca sorun giderme ile destek amacıyla kullanılır. Başarı
|
yalnızca sorun giderme ile destek amacıyla kullanılır. Başarı
|
||||||
sıralaman analitiğe ham hâliyle
|
sıralaman analitiğe ham hâliyle
|
||||||
gönderilmez; yalnızca “50-100 bin” gibi geniş bir
|
gönderilmez; yalnızca “50-100 bin” gibi geniş bir
|
||||||
aralık olarak kaydedilir.
|
aralık olarak kaydedilir. Danışman profilindeki değerler
|
||||||
|
analitiğe gitmez; yalnız hangi alanın kaydedildiği sayılır.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</Parallax>
|
</Parallax>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { URAP_DONEM, URAP_KAYNAK_URL, URAP_TOPLAM } from "@/lib/urap-siralama";
|
|||||||
import {
|
import {
|
||||||
YOK_ISTATISTIK_DONEM,
|
YOK_ISTATISTIK_DONEM,
|
||||||
YOK_ISTATISTIK_KAYNAK_URL,
|
YOK_ISTATISTIK_KAYNAK_URL,
|
||||||
|
kadinYuzdesi,
|
||||||
OGRENCI_BASINA_UYE_MEDYAN,
|
OGRENCI_BASINA_UYE_MEDYAN,
|
||||||
ogrenciBasinaUye,
|
ogrenciBasinaUye,
|
||||||
} from "@/lib/yok-istatistik";
|
} from "@/lib/yok-istatistik";
|
||||||
@@ -219,10 +220,19 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
// Kadın/erkek oranı bilgi satırıdır, gösterge değil: üniversite düzeyi
|
||||||
|
// (mühendislik ile hemşireliği aynı sayıya sokar), filtre ya da öncelik
|
||||||
|
// yapılmaz. Bu yüzden ayrı kare değil, öğrenci karesinin alt satırı.
|
||||||
|
const kadin = i ? kadinYuzdesi(i) : null;
|
||||||
const ogrenciKaresi = oranVar ? (
|
const ogrenciKaresi = oranVar ? (
|
||||||
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
||||||
<dd className={orta}>{sayi(i.ogrenci)}</dd>
|
<dd className={orta}>{sayi(i.ogrenci)}</dd>
|
||||||
<dt className={`mt-1.5 ${etiket}`}>Örgün öğrenci</dt>
|
<dt className={`mt-1.5 ${etiket}`}>Örgün öğrenci</dt>
|
||||||
|
{kadin != null ? (
|
||||||
|
<dd className="mt-2 text-sm font-bold text-white/90">
|
||||||
|
%{kadin} kadın · %{100 - kadin} erkek
|
||||||
|
</dd>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
const oranKaresi = oranVar ? (
|
const oranKaresi = oranVar ? (
|
||||||
@@ -375,7 +385,8 @@ function IstatistikKaynakSatiri({ uni }: { uni: UniDetay }) {
|
|||||||
{YOK_ISTATISTIK_DONEM}. Örgün öğrenci birinci ve ikinci öğretimdeki tüm
|
{YOK_ISTATISTIK_DONEM}. Örgün öğrenci birinci ve ikinci öğretimdeki tüm
|
||||||
düzeyleri (ön lisans–doktora) kapsar, uzaktan ve açık öğretim dahil
|
düzeyleri (ön lisans–doktora) kapsar, uzaktan ve açık öğretim dahil
|
||||||
değildir; öğretim üyesi profesör, doçent ve doktor öğretim üyesidir.
|
değildir; öğretim üyesi profesör, doçent ve doktor öğretim üyesidir.
|
||||||
Kaynak:{" "}
|
Kadın/erkek oranı örgün ön lisans ve lisans öğrencisine göredir ve
|
||||||
|
üniversite genelini anlatır; bölüm bazında veri yoktur. Kaynak:{" "}
|
||||||
<a
|
<a
|
||||||
href={YOK_ISTATISTIK_KAYNAK_URL}
|
href={YOK_ISTATISTIK_KAYNAK_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -77,15 +77,17 @@ export async function GirisKarti({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 text-center text-xs leading-relaxed text-slate-500">
|
<p className="mt-6 text-center text-xs leading-relaxed text-slate-500">
|
||||||
Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "}
|
{/* Aydınlatma "okudum" (bilgilendirme), koşullar "kabul": KVKK İlke
|
||||||
<Link href="/gizlilik" className="underline hover:text-slate-700">
|
Kararı 2026/347 aydınlatma ile rızayı ayırır (hukuk-ve-rakipler.md C.2/8). */}
|
||||||
gizlilik metnindeki
|
Giriş yaparak{" "}
|
||||||
</Link>{" "}
|
|
||||||
gibi işlenmesini ve{" "}
|
|
||||||
<Link href="/kosullar" className="underline hover:text-slate-700">
|
<Link href="/kosullar" className="underline hover:text-slate-700">
|
||||||
kullanım koşullarını
|
kullanım koşullarını
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
kabul etmiş olursun. KolayTercih yerleşme garantisi vermez.
|
kabul etmiş ve verilerinin nasıl işlendiğini anlatan{" "}
|
||||||
|
<Link href="/gizlilik" className="underline hover:text-slate-700">
|
||||||
|
gizlilik metnini
|
||||||
|
</Link>{" "}
|
||||||
|
okumuş olursun. KolayTercih yerleşme garantisi vermez.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ flowchart TD
|
|||||||
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, yalnızca ulaşabildiklerin listelenir")
|
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, yalnızca ulaşabildiklerin listelenir")
|
||||||
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
||||||
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
||||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar<br/>kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
|
C --> P("Danışmana söylediklerin<br/>öğretim dili, uzaktan/örgün, burs, yıllık ücret sınırı, evden uzaklık — sohbette söyleyip kaydettiklerin varsa havuzu bunlar da süzer")
|
||||||
|
P --> H("Aday havuzu<br/>elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar<br/>kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
|
||||||
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
||||||
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce il, sonra alan — listede açıkça belirtilir")
|
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce danışmana söylediklerin, sonra il, sonra alan — listede açıkça belirtilir")
|
||||||
G --> H
|
G --> H
|
||||||
H --> U("Havuz yine de dar kalırsa<br/>liste eldeki programlarla kurulur, dağılım havuza uyarlanır ve sana açıkça söylenir")
|
H --> U("Havuz yine de dar kalırsa<br/>liste eldeki programlarla kurulur, dağılım havuza uyarlanır ve sana açıkça söylenir")
|
||||||
S -- "hayır" --> Y("Yapay zekâ havuzdan en fazla 24 tercih seçer<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar")
|
S -- "hayır" --> Y("Yapay zekâ havuzdan en fazla 24 tercih seçer<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar")
|
||||||
@@ -34,7 +35,7 @@ flowchart TD
|
|||||||
classDef hayal fill:#fee2e2,stroke:#ef4444,color:#991b1b
|
classDef hayal fill:#fee2e2,stroke:#ef4444,color:#991b1b
|
||||||
classDef dengeli fill:#fef3c7,stroke:#f59e0b,color:#92400e
|
classDef dengeli fill:#fef3c7,stroke:#f59e0b,color:#92400e
|
||||||
classDef garanti fill:#d1fae5,stroke:#10b981,color:#065f46
|
classDef garanti fill:#d1fae5,stroke:#10b981,color:#065f46
|
||||||
class H,Y havuz
|
class H,Y,P havuz
|
||||||
class G,U uyari
|
class G,U uyari
|
||||||
class L1 hayal
|
class L1 hayal
|
||||||
class L2 dengeli
|
class L2 dengeli
|
||||||
@@ -44,12 +45,14 @@ export function HuniSemasi() {
|
|||||||
return (
|
return (
|
||||||
<ElCizimiSema
|
<ElCizimiSema
|
||||||
tanim={SEMA}
|
tanim={SEMA}
|
||||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla, danışmana söyleyip kaydettiğin tercihler (öğretim dili, uzaktan/örgün, burs, yıllık ücret sınırı, evden uzaklık) varsa onlarla da aday havuzuna daraltılır; havuza yalnızca en son tercih kılavuzunda yer alan programlar girer, kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler önce danışmana söylediklerin, sonra il, sonra alan sırasıyla gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||||
figcaption={
|
figcaption={
|
||||||
<>
|
<>
|
||||||
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
||||||
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez. Her
|
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez. Her
|
||||||
adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
adım havuzu biraz daha daraltır; danışmana söyleyip kaydettiğin
|
||||||
|
tercihler de (dil, ücret, uzaklık gibi) havuzu süzer. Yapay zekâ
|
||||||
|
listeyi bu havuzun{" "}
|
||||||
<span className="font-medium text-slate-700">
|
<span className="font-medium text-slate-700">
|
||||||
dışına asla çıkmadan
|
dışına asla çıkmadan
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
|
|||||||
@@ -114,15 +114,20 @@ export function MeraklisinaIcerik() {
|
|||||||
göre yeniden hesaplanarak listelenir; üçüncü adımdaki
|
göre yeniden hesaplanarak listelenir; üçüncü adımdaki
|
||||||
devlet/vakıf sayıları da hem alan hem il seçimini hesaba katar.
|
devlet/vakıf sayıları da hem alan hem il seçimini hesaba katar.
|
||||||
Seçtiğin il ve devlet/vakıf tercihi, sonuç sayfasındaki ücretsiz
|
Seçtiğin il ve devlet/vakıf tercihi, sonuç sayfasındaki ücretsiz
|
||||||
tabloya da uygulanır. Adımlar bitince elde kalan gerçek
|
tabloya da uygulanır. Danışmanla konuşurken söylediğin ve
|
||||||
|
“Kaydettiklerim” çiplerinde duran tercihler (öğretim
|
||||||
|
dili, uzaktan ya da örgün, burs, yıllık ücret sınırı, evden
|
||||||
|
uzaklık) varsa havuzu onlar da süzer; bir çipi silersen bir
|
||||||
|
sonraki liste onsuz kurulur. Adımlar bitince elde kalan gerçek
|
||||||
programlar bir <strong>aday havuzu</strong>{" "}
|
programlar bir <strong>aday havuzu</strong>{" "}
|
||||||
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
||||||
verilir. Havuza yalnızca{" "}
|
verilir. Havuza yalnızca{" "}
|
||||||
<strong>en son tercih kılavuzunda yer alan</strong>{" "}
|
<strong>en son tercih kılavuzunda yer alan</strong>{" "}
|
||||||
programlar girer: kapanmış ya da kodu değiştiği için eski kaydı
|
programlar girer: kapanmış ya da kodu değiştiği için eski kaydı
|
||||||
kalmış bir program sana önerilmez. Aşağıdaki şema akışın tamamını
|
kalmış bir program sana önerilmez. Aşağıdaki şema akışın tamamını
|
||||||
gösterir — havuz 24'ün altına düşerse filtrelerin nasıl
|
gösterir — havuz 24'ün altına düşerse filtrelerin hangi
|
||||||
gevşetildiği, yine de yetmezse listenin eldeki programlarla, sana
|
sırayla gevşetildiği (önce danışmana söylediklerin, sonra il,
|
||||||
|
sonra alan), yine de yetmezse listenin eldeki programlarla, sana
|
||||||
açıkça belirtilerek nasıl kurulduğu da dahil.
|
açıkça belirtilerek nasıl kurulduğu da dahil.
|
||||||
</p>
|
</p>
|
||||||
<HuniSemasi />
|
<HuniSemasi />
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
type DanismanOzeti,
|
type DanismanOzeti,
|
||||||
type IlkTercihYorumu,
|
type IlkTercihYorumu,
|
||||||
type Mesaj,
|
type Mesaj,
|
||||||
|
type ProfilSatiri,
|
||||||
} from "./sohbet-client";
|
} from "./sohbet-client";
|
||||||
import { RevizyonKutusu } from "./revizyon-kutusu";
|
import { RevizyonKutusu } from "./revizyon-kutusu";
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +56,7 @@ export function ListemGovde({
|
|||||||
kredi,
|
kredi,
|
||||||
raporKredi,
|
raporKredi,
|
||||||
sohbetGecmisi,
|
sohbetGecmisi,
|
||||||
|
danismanProfili,
|
||||||
ozet,
|
ozet,
|
||||||
ilkYorum,
|
ilkYorum,
|
||||||
kalanHak,
|
kalanHak,
|
||||||
@@ -76,6 +78,8 @@ export function ListemGovde({
|
|||||||
* kapıdan geçtiği için aynı bedeli gösterir. */
|
* kapıdan geçtiği için aynı bedeli gösterir. */
|
||||||
raporKredi: number;
|
raporKredi: number;
|
||||||
sohbetGecmisi: Mesaj[];
|
sohbetGecmisi: Mesaj[];
|
||||||
|
/** Danışmanın kaydettiği tercihler (sunucudan; çipler bununla açılır) */
|
||||||
|
danismanProfili: ProfilSatiri[];
|
||||||
ozet?: DanismanOzeti;
|
ozet?: DanismanOzeti;
|
||||||
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
||||||
ilkYorum?: IlkTercihYorumu;
|
ilkYorum?: IlkTercihYorumu;
|
||||||
@@ -124,6 +128,12 @@ export function ListemGovde({
|
|||||||
? "24'lük listenin tamamı: program, üniversite, şehir, taban sıralaması ve risk etiketi"
|
? "24'lük listenin tamamı: program, üniversite, şehir, taban sıralaması ve risk etiketi"
|
||||||
: `Listenin açık ilk ${acikSatir} satırı — kilitli tercihleri danışman da görmüyor`,
|
: `Listenin açık ilk ${acikSatir} satırı — kilitli tercihleri danışman da görmüyor`,
|
||||||
];
|
];
|
||||||
|
satirlar.push(
|
||||||
|
"Sohbette söylediğin ve \"Kaydettiklerim\" çiplerinde duran tercihler (ücret, il/km, dil, burs, öğretim türü, kampüs)",
|
||||||
|
);
|
||||||
|
satirlar.push(
|
||||||
|
"Sorunca veriden okur: taban sıraları, kontenjan, ücret/burs, dil, akreditasyon, koşullar, üniversite künyesi",
|
||||||
|
);
|
||||||
if (params.kategoriler?.length) {
|
if (params.kategoriler?.length) {
|
||||||
satirlar.push(`İlgi alanların: ${params.kategoriler.join(", ")}`);
|
satirlar.push(`İlgi alanların: ${params.kategoriler.join(", ")}`);
|
||||||
}
|
}
|
||||||
@@ -318,6 +328,7 @@ export function ListemGovde({
|
|||||||
<SohbetClient
|
<SohbetClient
|
||||||
baslangicKredi={kredi}
|
baslangicKredi={kredi}
|
||||||
initialMesajlar={sohbetGecmisi}
|
initialMesajlar={sohbetGecmisi}
|
||||||
|
initialProfil={danismanProfili}
|
||||||
ozet={ozet}
|
ozet={ozet}
|
||||||
ilkYorum={ilkYorum}
|
ilkYorum={ilkYorum}
|
||||||
bildikleri={danismaninBildikleri}
|
bildikleri={danismaninBildikleri}
|
||||||
@@ -329,6 +340,7 @@ export function ListemGovde({
|
|||||||
<SohbetClient
|
<SohbetClient
|
||||||
baslangicKredi={kredi}
|
baslangicKredi={kredi}
|
||||||
initialMesajlar={sohbetGecmisi}
|
initialMesajlar={sohbetGecmisi}
|
||||||
|
initialProfil={danismanProfili}
|
||||||
ozet={ozet}
|
ozet={ozet}
|
||||||
ilkYorum={ilkYorum}
|
ilkYorum={ilkYorum}
|
||||||
bildikleri={danismaninBildikleri}
|
bildikleri={danismaninBildikleri}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
cerezdenTercihProfili,
|
cerezdenTercihProfili,
|
||||||
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||||
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
|
import { profilSatirlari } from "@/lib/danisman-profil";
|
||||||
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
||||||
import type {
|
import type {
|
||||||
MaskeliRapor,
|
MaskeliRapor,
|
||||||
@@ -53,10 +55,11 @@ export async function ListemIcerik({
|
|||||||
? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}`
|
? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}`
|
||||||
: "/listem";
|
: "/listem";
|
||||||
const session = await verifySession(geriYol);
|
const session = await verifySession(geriYol);
|
||||||
const [user, satir, sohbetGecmisi] = await Promise.all([
|
const [user, satir, sohbetGecmisi, danismanProfili] = await Promise.all([
|
||||||
getCurrentUser(),
|
getCurrentUser(),
|
||||||
kullanicininRaporu(session.user.id),
|
kullanicininRaporu(session.user.id),
|
||||||
sohbetGecmisiGetir(session.user.id),
|
sohbetGecmisiGetir(session.user.id),
|
||||||
|
profilGetir(session.user.id),
|
||||||
]);
|
]);
|
||||||
if (!user) redirect(`/giris?callback=${encodeURIComponent(geriYol)}`);
|
if (!user) redirect(`/giris?callback=${encodeURIComponent(geriYol)}`);
|
||||||
|
|
||||||
@@ -171,6 +174,7 @@ export async function ListemIcerik({
|
|||||||
user.hasPaket ? 0 : RAPOR_KREDI
|
user.hasPaket ? 0 : RAPOR_KREDI
|
||||||
}
|
}
|
||||||
sohbetGecmisi={sohbetGecmisi as Mesaj[]}
|
sohbetGecmisi={sohbetGecmisi as Mesaj[]}
|
||||||
|
danismanProfili={profilSatirlari(danismanProfili)}
|
||||||
ozet={danismanOzeti}
|
ozet={danismanOzeti}
|
||||||
ilkYorum={ilkYorum}
|
ilkYorum={ilkYorum}
|
||||||
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { startTransition, useEffect, useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2 } from "lucide-react";
|
import { ArrowUp, Coins, Info, Lock, Sparkles, Trash2, X } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||||
@@ -42,6 +42,10 @@ import {
|
|||||||
import { DotsLoader } from "@/components/ui/loader";
|
import { DotsLoader } from "@/components/ui/loader";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { olay } from "@/lib/analitik";
|
import { olay } from "@/lib/analitik";
|
||||||
|
import type { ProfilAlani } from "@/lib/danisman-profil";
|
||||||
|
|
||||||
|
/** /api/soru/profil'in döndüğü, öğrenciye okunan profil satırı. */
|
||||||
|
export type ProfilSatiri = { alan: ProfilAlani; metin: string };
|
||||||
|
|
||||||
export interface Mesaj {
|
export interface Mesaj {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -123,8 +127,11 @@ export function SohbetClient({
|
|||||||
ozet,
|
ozet,
|
||||||
ilkYorum,
|
ilkYorum,
|
||||||
bildikleri,
|
bildikleri,
|
||||||
|
initialProfil,
|
||||||
}: {
|
}: {
|
||||||
baslangicKredi: number;
|
baslangicKredi: number;
|
||||||
|
/** Sunucu tarafında çekilmiş danışman profili; verilirse mount fetch'i atlanır */
|
||||||
|
initialProfil?: ProfilSatiri[];
|
||||||
/** Sunucu tarafında çekilmiş geçmiş; verilirse mount fetch'i atlanır */
|
/** Sunucu tarafında çekilmiş geçmiş; verilirse mount fetch'i atlanır */
|
||||||
initialMesajlar?: Mesaj[];
|
initialMesajlar?: Mesaj[];
|
||||||
/** Boyutlandırma override'ı (ör. alt popup'ta h-full) */
|
/** Boyutlandırma override'ı (ör. alt popup'ta h-full) */
|
||||||
@@ -146,8 +153,57 @@ export function SohbetClient({
|
|||||||
const [kredi, setKredi] = useState(baslangicKredi);
|
const [kredi, setKredi] = useState(baslangicKredi);
|
||||||
const [temizleAcik, setTemizleAcik] = useState(false);
|
const [temizleAcik, setTemizleAcik] = useState(false);
|
||||||
const [temizleniyor, setTemizleniyor] = useState(false);
|
const [temizleniyor, setTemizleniyor] = useState(false);
|
||||||
|
// Danışmanın sohbetten çıkarıp kaydettiği tercihler (kapalı listeli profil).
|
||||||
|
// Başlığın altında çip olarak durur; çarpı tek alanı siler. Cevap sonrası
|
||||||
|
// yeniden çekilir: model profil_guncelle çağırdıysa yeni çip burada belirir.
|
||||||
|
const [profil, setProfil] = useState<ProfilSatiri[]>(initialProfil ?? []);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function profiliCek(oncekiAlanlar: Set<ProfilAlani>) {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/soru/profil");
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = (await res.json()) as { satirlar?: ProfilSatiri[] };
|
||||||
|
const satirlar = data.satirlar ?? [];
|
||||||
|
setProfil(satirlar);
|
||||||
|
for (const s of satirlar) {
|
||||||
|
// Analitiğe yalnız alan adı gider, değer gitmez (KVKK)
|
||||||
|
if (!oncekiAlanlar.has(s.alan)) olay("profil_alani_kaydedildi", { alan: s.alan });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Listem sayfası profili sunucudan indirir; /sonuc popup'ı gibi geç mount
|
||||||
|
// olan yerler çeker (ttlMs 0: yalnız in-flight dedup, DELETE sonrası
|
||||||
|
// bayat kalmasın).
|
||||||
|
if (initialProfil != null) return;
|
||||||
|
onbellekliJsonGetir<{ satirlar?: ProfilSatiri[] }>("/api/soru/profil", {
|
||||||
|
ttlMs: 0,
|
||||||
|
})
|
||||||
|
.then((data) => setProfil(data?.satirlar ?? []))
|
||||||
|
.catch(() => {});
|
||||||
|
}, [initialProfil]);
|
||||||
|
|
||||||
|
async function profilAlaniSil(alan: ProfilAlani) {
|
||||||
|
const onceki = profil;
|
||||||
|
setProfil((p) => p.filter((s) => s.alan !== alan));
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/soru/profil", {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ alan }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("silinemedi");
|
||||||
|
const data = (await res.json()) as { satirlar?: ProfilSatiri[] };
|
||||||
|
setProfil(data.satirlar ?? []);
|
||||||
|
olay("profil_alani_silindi", { alan });
|
||||||
|
} catch {
|
||||||
|
setProfil(onceki);
|
||||||
|
toast.error("Silinemedi, tekrar dener misin?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown chunk'ını sohbet görünür olur olmaz ısıt: ilk danışman cevabı
|
// Markdown chunk'ını sohbet görünür olur olmaz ısıt: ilk danışman cevabı
|
||||||
// (veya geçmiş mesajlar) çizilirken lazy sınırında düz-metin flash'ı olmasın.
|
// (veya geçmiş mesajlar) çizilirken lazy sınırında düz-metin flash'ı olmasın.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -213,7 +269,8 @@ export function SohbetClient({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setKredi((k) => k - 1);
|
// Profil onayı ("kaydedeyim mi?" → "evet") sunucuda bedelsizdir
|
||||||
|
if (res.headers.get("X-Kredi-Dusuldu") !== "0") setKredi((k) => k - 1);
|
||||||
// Bu bileşen /sonuc'taki popup'tan da kullanılıyor (sohbet-popup.tsx)
|
// Bu bileşen /sonuc'taki popup'tan da kullanılıyor (sohbet-popup.tsx)
|
||||||
olay("danisman_sorusu", {
|
olay("danisman_sorusu", {
|
||||||
sayfa: location.pathname.startsWith("/listem") ? "listem" : "sonuc",
|
sayfa: location.pathname.startsWith("/listem") ? "listem" : "sonuc",
|
||||||
@@ -236,6 +293,8 @@ export function SohbetClient({
|
|||||||
// Header'daki kredi pill'i layout'ta server-render edilir ve client
|
// Header'daki kredi pill'i layout'ta server-render edilir ve client
|
||||||
// harcamasını görmez; bakiye orada da güncellensin.
|
// harcamasını görmez; bakiye orada da güncellensin.
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
// Danışman bu cevapta profil_guncelle çağırmış olabilir
|
||||||
|
void profiliCek(new Set(profil.map((s) => s.alan)));
|
||||||
} catch {
|
} catch {
|
||||||
// Sunucu hata anında krediyi iade edip yarım cevabı kaydetmiş olabilir;
|
// Sunucu hata anında krediyi iade edip yarım cevabı kaydetmiş olabilir;
|
||||||
// tahmin etme: kayıtlı geçmişi ve bakiyeyi sunucudan çek (boş balon da
|
// tahmin etme: kayıtlı geçmişi ve bakiyeyi sunucudan çek (boş balon da
|
||||||
@@ -358,6 +417,35 @@ export function SohbetClient({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{profil.length > 0 ? (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-label="Danışmanın kaydettiği tercihlerin"
|
||||||
|
className="flex flex-wrap items-center gap-2 border-b border-slate-100 px-4 py-2.5"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-semibold text-slate-500">
|
||||||
|
Kaydettiklerim
|
||||||
|
</span>
|
||||||
|
{profil.map((s) => (
|
||||||
|
// Etkileşimli çip (§4.17 kalıbı, kompakt h-8): rozet değil.
|
||||||
|
<span
|
||||||
|
key={s.alan}
|
||||||
|
className="inline-flex h-8 items-center gap-1 rounded-full border border-slate-200 bg-white pr-1 pl-3 text-xs font-semibold text-slate-700"
|
||||||
|
>
|
||||||
|
{s.metin}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => profilAlaniSil(s.alan)}
|
||||||
|
aria-label={`Sil: ${s.metin}`}
|
||||||
|
className="inline-flex size-6 cursor-pointer items-center justify-center rounded-full text-slate-500 transition-[background-color,color,transform] duration-150 hover:bg-slate-100 hover:text-orange-500 active:scale-[0.96]"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" aria-hidden />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Dialog open={temizleAcik} onOpenChange={setTemizleAcik}>
|
<Dialog open={temizleAcik} onOpenChange={setTemizleAcik}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -576,6 +664,11 @@ export function SohbetClient({
|
|||||||
</PromptInputAction>
|
</PromptInputAction>
|
||||||
</PromptInputActions>
|
</PromptInputActions>
|
||||||
</PromptInput>
|
</PromptInput>
|
||||||
|
{/* KVKK: sohbete kişisel bilgi yazılmasın (hukuk-ve-rakipler.md C.5/22) */}
|
||||||
|
<p className="mt-2 px-1 text-xs leading-5 text-slate-500">
|
||||||
|
Ad, telefon, kimlik numarası ya da sağlık bilgisi yazma; danışmanın
|
||||||
|
bunlara ihtiyacı yok.
|
||||||
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
uretimKilidiBirak,
|
uretimKilidiBirak,
|
||||||
} from "@/lib/uretim-kilidi";
|
} from "@/lib/uretim-kilidi";
|
||||||
import { getSession, getCurrentUser } from "@/lib/session";
|
import { getSession, getCurrentUser } from "@/lib/session";
|
||||||
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
import { appDb, schema } from "@/lib/appdb";
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||||
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
||||||
@@ -214,7 +215,9 @@ async function listeOlusturKilitli(
|
|||||||
|
|
||||||
let sonuc: RaporSonuc;
|
let sonuc: RaporSonuc;
|
||||||
try {
|
try {
|
||||||
sonuc = await raporUret(params);
|
// Danışmana söylenen tercihler (varsa) havuzu süzer; okunamazsa profilsiz üret
|
||||||
|
const profil = await profilGetir(user.id).catch(() => undefined);
|
||||||
|
sonuc = await raporUret(params, undefined, undefined, profil);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Üretim başarısız → krediyi iade et. Satır paketlide de yazılır (delta 0):
|
// Üretim başarısız → krediyi iade et. Satır paketlide de yazılır (delta 0):
|
||||||
// yukarıdaki DUPLICATE dalı "önceki deneme başarısız mıydı" sorusunu tam
|
// yukarıdaki DUPLICATE dalı "önceki deneme başarısız mıydı" sorusunu tam
|
||||||
@@ -347,10 +350,12 @@ export async function listeRevize(input: {
|
|||||||
|
|
||||||
let sonuc: RaporSonuc;
|
let sonuc: RaporSonuc;
|
||||||
try {
|
try {
|
||||||
|
const profil = await profilGetir(user.id).catch(() => undefined);
|
||||||
sonuc = await raporUret(
|
sonuc = await raporUret(
|
||||||
params,
|
params,
|
||||||
feedback.slice(0, 1000),
|
feedback.slice(0, 1000),
|
||||||
listeOzetiCikar(onceki, params.sira),
|
listeOzetiCikar(onceki, params.sira),
|
||||||
|
profil,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// İade satırı burada da yazılır (paketlide delta 0) — DUPLICATE dalındaki
|
// İade satırı burada da yazılır (paketlide delta 0) — DUPLICATE dalındaki
|
||||||
|
|||||||
456
src/lib/ai/araclar.ts
Normal file
456
src/lib/ai/araclar.ts
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
/**
|
||||||
|
* Danışmanın araçları (OpenAI uyumlu function calling, OpenRouter üzerinden).
|
||||||
|
*
|
||||||
|
* İki tür: SORGU araçları salt okunur — model sayı uydurmaz, DB'den okur;
|
||||||
|
* `profil_guncelle` sohbetten çıkarılan tercihi KAPALI LİSTELİ profile yazar
|
||||||
|
* (src/lib/danisman-profil.ts). Yapay zekâ listeyi araçla KURMAZ: 24'lük liste
|
||||||
|
* üretimi deterministik havuzda kalır (havuz-dışına-çıkamama kuralı).
|
||||||
|
*
|
||||||
|
* Araç sonuçları kısa ve yapısal döner (prompt bütçesi; sohbet 1024 token).
|
||||||
|
*/
|
||||||
|
import type OpenAI from "openai";
|
||||||
|
import {
|
||||||
|
dilimAraligi,
|
||||||
|
EFEKTIF_SIRA,
|
||||||
|
getDb,
|
||||||
|
programGetir,
|
||||||
|
programSatiri,
|
||||||
|
SELECT_COLS,
|
||||||
|
type ProgramHamSatiri,
|
||||||
|
} from "@/lib/db";
|
||||||
|
import {
|
||||||
|
kosulMetinleri,
|
||||||
|
ogretimUyesiToplami,
|
||||||
|
ozellikOzeti,
|
||||||
|
programOzellikGetir,
|
||||||
|
programOzellikleriGetir,
|
||||||
|
type ProgramOzellik,
|
||||||
|
} from "@/lib/program-ozellik";
|
||||||
|
import { getAllUniversiteler, getUniversiteBySlug } from "@/lib/katalog";
|
||||||
|
import { kadinYuzdesi, ogrenciBasinaUye, YOK_ISTATISTIK_DONEM } from "@/lib/yok-istatistik";
|
||||||
|
import { URAP_DONEM, URAP_TOPLAM } from "@/lib/urap-siralama";
|
||||||
|
import { turkishSlugify } from "@/lib/slug";
|
||||||
|
import { DILIM_ETIKET } from "@/lib/risk";
|
||||||
|
import { YILLAR } from "@/lib/veri-yillari";
|
||||||
|
import { PUAN_TURLERI, type Program, type PuanTuruKey } from "@/types/yokatlas";
|
||||||
|
import {
|
||||||
|
PROFIL_ALAN_ACIKLAMA,
|
||||||
|
PROFIL_ALANLARI,
|
||||||
|
PROFIL_SECENEKLERI,
|
||||||
|
profilSatirlari,
|
||||||
|
type DanismanProfili,
|
||||||
|
type ProfilAlani,
|
||||||
|
} from "@/lib/danisman-profil";
|
||||||
|
import { profilAlanYaz, profilGetir } from "@/lib/danisman-profil-db";
|
||||||
|
import { ilAdiNormalize, illerArasiKm, yakinIller } from "@/lib/mesafe";
|
||||||
|
|
||||||
|
export type AracBaglami = {
|
||||||
|
userId: string;
|
||||||
|
sira: number;
|
||||||
|
tur: PuanTuruKey;
|
||||||
|
/** Sohbet başındaki profil; profil_guncelle sonrası yenilenir */
|
||||||
|
profil: DanismanProfili;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AracTanimi = OpenAI.Chat.Completions.ChatCompletionTool;
|
||||||
|
|
||||||
|
const ARAMA_LIMIT = 8;
|
||||||
|
|
||||||
|
export const ARACLAR: AracTanimi[] = [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "program_ara",
|
||||||
|
description:
|
||||||
|
"Adayın puan türündeki programları veriden arar (YÖK Atlas, son yerleştirme). Bölüm adı, üniversite, il, öğretim dili, burs ve yıllık ücret üst sınırına göre süzer; adayın sırasına en yakın programları döner. Sayı uydurmak yerine bunu çağır.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
arama: { type: "string", description: "Bölüm adı parçası, ör. 'bilgisayar mühendisliği', 'hukuk'" },
|
||||||
|
universite: { type: "string", description: "Üniversite adı parçası" },
|
||||||
|
il: { type: "string", description: "İl adı, ör. 'İzmir'" },
|
||||||
|
dil: { type: "string", enum: ["Türkçe", "İngilizce"], description: "Öğretim dili" },
|
||||||
|
burs: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["Burslu", "%50 İndirimli", "%25 İndirimli", "Ücretli"],
|
||||||
|
description: "Vakıf burs oranı",
|
||||||
|
},
|
||||||
|
ucret_ust: { type: "integer", description: "Yıllık ücret üst sınırı (TL); devlet programları her zaman geçer" },
|
||||||
|
dilim: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["hayal", "dengeli", "guvenli"],
|
||||||
|
description: "Yalnız bu dilimdekiler: hayal = adayın sırasının üstünde kapananlar, dengeli = sırasının 1,4 katına kadar, guvenli = daha altı",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "program_detay",
|
||||||
|
description:
|
||||||
|
"Tek programın ayrıntısı: yıllara göre taban sırası ve kontenjan, ücret/burs, dil, öğretim türü, akreditasyon, kadro, tercih barajı ve kılavuzdaki yerleşme koşulları (sağlık şartı, mülakat vb.).",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
program_id: { type: "string", description: "program_ara sonucundaki id (kılavuz kodu)" },
|
||||||
|
},
|
||||||
|
required: ["program_id"],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "universite_profili",
|
||||||
|
description:
|
||||||
|
"Üniversitenin künyesi: il, devlet/vakıf, program sayısı, URAP araştırma sırası, örgün öğrenci sayısı, hoca başına öğrenci ve kadın/erkek öğrenci oranı (üniversite geneli, bölüm bazında veri yok).",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
universite: { type: "string", description: "Üniversite adı, ör. 'Boğaziçi', 'ODTÜ', 'Ege Üniversitesi'" },
|
||||||
|
},
|
||||||
|
required: ["universite"],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "profil_guncelle",
|
||||||
|
description:
|
||||||
|
"Adayın sohbette söylediği bir tercihi profile kaydeder ya da siler (deger null). YALNIZ şu alanlar: " +
|
||||||
|
PROFIL_ALANLARI.map((a) => `${a} (${PROFIL_ALAN_ACIKLAMA[a]})`).join("; ") +
|
||||||
|
". Sağlık, engel, din, aile, gelir, cinsiyet gibi bilgileri ASLA kaydetme. Kaydettikten sonra cevabında tek cümleyle 'Şunu kaydettim: …' de.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
alan: { type: "string", enum: PROFIL_ALANLARI },
|
||||||
|
deger: {
|
||||||
|
type: ["string", "null"],
|
||||||
|
description:
|
||||||
|
"evIl: il adı; maxKm: sayı (km); ucretUst: sayı (TL, yıllık); " +
|
||||||
|
`burs: ${Object.keys(PROFIL_SECENEKLERI.burs).join("|")}; ` +
|
||||||
|
`dil: ${Object.keys(PROFIL_SECENEKLERI.dil).join("|")}; ` +
|
||||||
|
`ogretimTuru: ${Object.keys(PROFIL_SECENEKLERI.ogretimTuru).join("|")}; ` +
|
||||||
|
`kampus: ${Object.keys(PROFIL_SECENEKLERI.kampus).join("|")}; null = alanı sil`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["alan", "deger"],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Yardımcılar ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function dilimAdi(sira: number, efektif: number | null): string {
|
||||||
|
if (efektif == null) return "Veri yetersiz";
|
||||||
|
if (efektif < sira) return DILIM_ETIKET.hayal;
|
||||||
|
if (efektif <= dilimAraligi(sira, "dengeli").ust!) return DILIM_ETIKET.dengeli;
|
||||||
|
return DILIM_ETIKET.garanti;
|
||||||
|
}
|
||||||
|
|
||||||
|
function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
program: p.isim,
|
||||||
|
universite: p.universite,
|
||||||
|
il: p.il,
|
||||||
|
tur: p.unitur,
|
||||||
|
tabanSira: p.efektifSira,
|
||||||
|
dilim: dilimAdi(sira, p.efektifSira),
|
||||||
|
kontenjan: p.kontenjanSon,
|
||||||
|
ozellik: o ? ozellikOzeti(o) : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SQLite LIKE Türkçe büyük/küçük harfi bilmez: hem yazıldığı gibi hem başlık düzeni denenir. */
|
||||||
|
function likeVaryantlari(q: string): string[] {
|
||||||
|
const t = q.trim();
|
||||||
|
const baslik = t
|
||||||
|
.toLocaleLowerCase("tr-TR")
|
||||||
|
.split(" ")
|
||||||
|
.map((w) => (w ? w[0].toLocaleUpperCase("tr-TR") + w.slice(1) : w))
|
||||||
|
.join(" ");
|
||||||
|
return Array.from(new Set([t, baslik, t.toLocaleUpperCase("tr-TR")]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function likeKosulu(kolon: string, q: string): { sql: string; args: string[] } {
|
||||||
|
const v = likeVaryantlari(q);
|
||||||
|
return {
|
||||||
|
sql: `(${v.map(() => `${kolon} LIKE ?`).join(" OR ")})`,
|
||||||
|
args: v.map((x) => `%${x}%`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Araç gövdeleri ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ProgramAraArg = {
|
||||||
|
arama?: string;
|
||||||
|
universite?: string;
|
||||||
|
il?: string;
|
||||||
|
dil?: string;
|
||||||
|
burs?: string;
|
||||||
|
ucret_ust?: number;
|
||||||
|
dilim?: "hayal" | "dengeli" | "guvenli";
|
||||||
|
};
|
||||||
|
|
||||||
|
function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): ProgramAraSonuc {
|
||||||
|
const db = getDb();
|
||||||
|
const profil: DanismanProfili = profilUygula ? b.profil : {};
|
||||||
|
const kosullar: string[] = ["tur = ?", "onlisans = ?", `${EFEKTIF_SIRA} IS NOT NULL`];
|
||||||
|
const args: (string | number)[] = [PUAN_TURLERI[b.tur], b.tur === "tyt" ? 1 : 0];
|
||||||
|
const uygulanan: string[] = [];
|
||||||
|
|
||||||
|
if (a.arama) {
|
||||||
|
const k = likeKosulu("isim", a.arama);
|
||||||
|
kosullar.push(k.sql);
|
||||||
|
args.push(...k.args);
|
||||||
|
uygulanan.push(`bölüm ~ "${a.arama}"`);
|
||||||
|
}
|
||||||
|
if (a.universite) {
|
||||||
|
const k = likeKosulu("universite", a.universite);
|
||||||
|
kosullar.push(k.sql);
|
||||||
|
args.push(...k.args);
|
||||||
|
uygulanan.push(`üniversite ~ "${a.universite}"`);
|
||||||
|
}
|
||||||
|
// İl: parametre > profildeki km sınırı
|
||||||
|
if (a.il) {
|
||||||
|
const il = ilAdiNormalize(a.il);
|
||||||
|
if (!il) {
|
||||||
|
return { toplamEslesen: 0, gosterilen: 0, uygulananFiltreler: [], sonuclar: [], not: `İl tanınmadı: ${a.il}` };
|
||||||
|
}
|
||||||
|
kosullar.push("il = ?");
|
||||||
|
args.push(il);
|
||||||
|
uygulanan.push(`il = ${il}`);
|
||||||
|
} else if (profil.evIl && profil.maxKm) {
|
||||||
|
const iller = yakinIller(profil.evIl, profil.maxKm);
|
||||||
|
if (iller.length) {
|
||||||
|
kosullar.push(`il IN (${iller.map(() => "?").join(",")})`);
|
||||||
|
args.push(...iller);
|
||||||
|
uygulanan.push(`profil: ${profil.evIl} iline en fazla ${profil.maxKm} km (${iller.length} il)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Özellik süzgeçleri alt sorguda (program_ozellik.id çakışmasın)
|
||||||
|
const ozKosul: string[] = [];
|
||||||
|
const ozArgs: (string | number)[] = [];
|
||||||
|
const dil = a.dil ?? (profil.dil === "ingilizce" ? "İngilizce" : profil.dil === "turkce" ? "Türkçe" : undefined);
|
||||||
|
if (dil) {
|
||||||
|
ozKosul.push("ogrenim_dili LIKE ?");
|
||||||
|
ozArgs.push(`${dil}%`);
|
||||||
|
uygulanan.push(`dil = ${dil}${a.dil ? "" : " (profil)"}`);
|
||||||
|
}
|
||||||
|
if (a.burs) {
|
||||||
|
ozKosul.push("burs_orani = ?");
|
||||||
|
ozArgs.push(a.burs);
|
||||||
|
uygulanan.push(`burs = ${a.burs}`);
|
||||||
|
} else if (profil.burs === "yalniz-burslu") {
|
||||||
|
ozKosul.push("(burs_orani IS NULL OR burs_orani = 'Burslu')");
|
||||||
|
uygulanan.push("profil: vakıfta yalnız burslu");
|
||||||
|
}
|
||||||
|
const ucretUst = a.ucret_ust ?? profil.ucretUst;
|
||||||
|
if (ucretUst) {
|
||||||
|
ozKosul.push("(ucret IS NULL OR burs_orani = 'Burslu' OR ucret <= ?)");
|
||||||
|
ozArgs.push(ucretUst);
|
||||||
|
uygulanan.push(`yıllık ücret ≤ ${ucretUst.toLocaleString("tr-TR")} TL${a.ucret_ust ? "" : " (profil)"}`);
|
||||||
|
}
|
||||||
|
if (profil.ogretimTuru === "yalniz-orgun") {
|
||||||
|
ozKosul.push("(ogrenim_turu IS NULL OR ogrenim_turu = 'Örgün Öğretim')");
|
||||||
|
uygulanan.push("profil: yalnız örgün");
|
||||||
|
}
|
||||||
|
if (ozKosul.length) {
|
||||||
|
kosullar.push(`id IN (SELECT id FROM program_ozellik WHERE ${ozKosul.join(" AND ")})`);
|
||||||
|
args.push(...ozArgs);
|
||||||
|
}
|
||||||
|
if (a.dilim) {
|
||||||
|
const anahtar = a.dilim === "guvenli" ? "garanti" : a.dilim;
|
||||||
|
const { alt, ust } = dilimAraligi(b.sira, anahtar);
|
||||||
|
kosullar.push(ust != null ? `${EFEKTIF_SIRA} BETWEEN ? AND ?` : `${EFEKTIF_SIRA} >= ?`);
|
||||||
|
args.push(alt);
|
||||||
|
if (ust != null) args.push(ust);
|
||||||
|
uygulanan.push(`dilim = ${DILIM_ETIKET[anahtar]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const satirlar = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT ${SELECT_COLS} FROM programs WHERE ${kosullar.join(" AND ")}
|
||||||
|
ORDER BY ABS(${EFEKTIF_SIRA} - ?) ASC LIMIT ?`,
|
||||||
|
)
|
||||||
|
.all(...args, b.sira, ARAMA_LIMIT) as ProgramHamSatiri[];
|
||||||
|
const programlar = satirlar.map(programSatiri);
|
||||||
|
const toplam = (
|
||||||
|
db.prepare(`SELECT COUNT(*) AS n FROM programs WHERE ${kosullar.join(" AND ")}`).get(...args) as {
|
||||||
|
n: number;
|
||||||
|
}
|
||||||
|
).n;
|
||||||
|
// Profil süzgeçleri sonucu sıfırladıysa profilsiz tekrar dene ve bunu söyle:
|
||||||
|
// model "hiç yok" demek yerine "profilin dışında şunlar var" diyebilsin.
|
||||||
|
if (toplam === 0 && profilUygula && uygulanan.some((u) => u.includes("profil"))) {
|
||||||
|
const gevsek = programAra(b, a, false);
|
||||||
|
return {
|
||||||
|
...gevsek,
|
||||||
|
not: `Profil süzgeçleriyle (${uygulanan.filter((u) => u.includes("profil")).join("; ")}) eşleşen program YOK; aşağıdakiler profil süzgeci olmadan bulundu — adaya bunu açıkça söyle. ${gevsek.not}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ozellikler = programOzellikleriGetir(programlar.map((p) => p.id));
|
||||||
|
return {
|
||||||
|
toplamEslesen: toplam,
|
||||||
|
gosterilen: programlar.length,
|
||||||
|
uygulananFiltreler: uygulanan,
|
||||||
|
sonuclar: programlar.map((p) => programOzeti(p, b.sira, ozellikler[p.id])),
|
||||||
|
not: "tabanSira son yerleştirme yılının taban başarı sırası; her yıl değişir.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgramAraSonuc = {
|
||||||
|
toplamEslesen: number;
|
||||||
|
gosterilen: number;
|
||||||
|
uygulananFiltreler: string[];
|
||||||
|
sonuclar: ReturnType<typeof programOzeti>[];
|
||||||
|
not: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function programDetay(b: AracBaglami, a: { program_id?: string }) {
|
||||||
|
const id = String(a.program_id ?? "").trim();
|
||||||
|
const p = id ? programGetir(id) : null;
|
||||||
|
if (!p) return { hata: "Program bulunamadı; önce program_ara ile id al." };
|
||||||
|
const o = programOzellikGetir(id);
|
||||||
|
const kosullar = o ? kosulMetinleri(o.kosulKodlari).slice(0, 8) : [];
|
||||||
|
const evKm = b.profil.evIl && p.il ? illerArasiKm(b.profil.evIl, p.il) : null;
|
||||||
|
return {
|
||||||
|
...programOzeti(p, b.sira, o ?? undefined),
|
||||||
|
fakulte: p.fakulte,
|
||||||
|
sure: p.sure,
|
||||||
|
tabanSiraYillara: Object.fromEntries(YILLAR.map((y, i) => [y, p.siraGecmisi[i]])),
|
||||||
|
puanSon: p.puanSon,
|
||||||
|
yerlesenSon: p.yerlesenSon,
|
||||||
|
ucretYillik: o?.ucret ?? null,
|
||||||
|
bursOrani: o?.bursOrani ?? null,
|
||||||
|
ogretimDili: o?.ogrenimDili ?? null,
|
||||||
|
ogretimTuru: o?.ogrenimTuru ?? null,
|
||||||
|
akreditasyon: o?.akreditasyon ?? null,
|
||||||
|
kadro: o
|
||||||
|
? { prof: o.prof, docent: o.doc, drOgrUyesi: o.dou, arsGor: o.argor, ogretimUyesiToplam: ogretimUyesiToplami(o) }
|
||||||
|
: null,
|
||||||
|
tercihBaraji: o?.minBasariSirasi ?? null,
|
||||||
|
ilce: o?.ilce ?? null,
|
||||||
|
evdenKm: evKm,
|
||||||
|
kosullar: kosullar.map((k) => (k.metin.length > 240 ? `${k.metin.slice(0, 237)}…` : k.metin)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function universiteProfili(_b: AracBaglami, a: { universite?: string }) {
|
||||||
|
const q = String(a.universite ?? "").trim();
|
||||||
|
if (!q) return { hata: "Üniversite adı gerekli." };
|
||||||
|
const slugQ = turkishSlugify(q);
|
||||||
|
const hepsi = getAllUniversiteler();
|
||||||
|
const kisaltma: Record<string, string> = {
|
||||||
|
odtu: "orta-dogu-teknik",
|
||||||
|
itu: "istanbul-teknik",
|
||||||
|
ytu: "yildiz-teknik",
|
||||||
|
ktu: "karadeniz-teknik",
|
||||||
|
dtu: "deu",
|
||||||
|
};
|
||||||
|
const arananSlug = kisaltma[slugQ] ?? slugQ;
|
||||||
|
const aday =
|
||||||
|
hepsi.find((u) => u.slug === arananSlug || u.slug === `${arananSlug}-universitesi`) ??
|
||||||
|
hepsi.find((u) => u.slug.startsWith(arananSlug)) ??
|
||||||
|
hepsi.find((u) => u.slug.includes(arananSlug));
|
||||||
|
if (!aday) return { hata: `Üniversite bulunamadı: ${q}` };
|
||||||
|
const u = getUniversiteBySlug(aday.slug);
|
||||||
|
if (!u) return { hata: `Üniversite bulunamadı: ${q}` };
|
||||||
|
const i = u.istatistik;
|
||||||
|
const kadin = i ? kadinYuzdesi(i) : null;
|
||||||
|
return {
|
||||||
|
ad: u.ad,
|
||||||
|
il: u.il,
|
||||||
|
tur: u.unitur,
|
||||||
|
programSayisi: u.programSayisi,
|
||||||
|
enDusukTabanSira: u.stats.minSira,
|
||||||
|
toplamKontenjan: u.stats.toplamKontenjan,
|
||||||
|
urap: u.urap ? { sira: u.urap.sira, toplam: URAP_TOPLAM, donem: URAP_DONEM, olctugu: "araştırma performansı; eğitim kalitesi ya da iş bulma değil" } : null,
|
||||||
|
istatistik: i
|
||||||
|
? {
|
||||||
|
donem: YOK_ISTATISTIK_DONEM,
|
||||||
|
orgunOgrenci: i.ogrenci,
|
||||||
|
ogretimUyesi: i.ogretimUyesi,
|
||||||
|
hocaBasinaOgrenci: ogrenciBasinaUye(i),
|
||||||
|
kadinYuzdesi: kadin,
|
||||||
|
erkekYuzdesi: kadin == null ? null : 100 - kadin,
|
||||||
|
not: "Kadın/erkek oranı örgün ön lisans + lisans, ÜNİVERSİTE GENELİ; bölüm bazında veri yok. Bilgi olarak söyle, filtre yapma.",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
sayfa: `/universite/${u.slug}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sayiParse(v: unknown): number | null {
|
||||||
|
if (typeof v === "number") return v;
|
||||||
|
if (typeof v !== "string") return null;
|
||||||
|
const t = v.toLocaleLowerCase("tr-TR").replace(/\s/g, "");
|
||||||
|
const m = t.match(/^([\d.,]+)(bin|k|milyon|m)?/);
|
||||||
|
if (!m) return null;
|
||||||
|
let n = Number.parseFloat(m[1].replace(/\./g, "").replace(",", "."));
|
||||||
|
if (!Number.isFinite(n)) return null;
|
||||||
|
if (m[2] === "bin" || m[2] === "k") n *= 1000;
|
||||||
|
if (m[2] === "milyon" || m[2] === "m") n *= 1_000_000;
|
||||||
|
return Math.round(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function profilGuncelle(b: AracBaglami, a: { alan?: string; deger?: unknown }) {
|
||||||
|
const alan = a.alan as ProfilAlani;
|
||||||
|
if (!PROFIL_ALANLARI.includes(alan)) {
|
||||||
|
return { hata: `Bilinmeyen alan: ${String(a.alan)}. Yalnız ${PROFIL_ALANLARI.join(", ")} kaydedilebilir.` };
|
||||||
|
}
|
||||||
|
let deger: unknown = a.deger;
|
||||||
|
if (deger !== null && deger !== undefined) {
|
||||||
|
if (alan === "maxKm" || alan === "ucretUst") {
|
||||||
|
deger = sayiParse(deger);
|
||||||
|
if (deger == null) return { hata: "Sayı okunamadı." };
|
||||||
|
} else if (alan === "evIl") {
|
||||||
|
const il = ilAdiNormalize(String(deger));
|
||||||
|
if (!il) return { hata: `İl tanınmadı: ${String(deger)}` };
|
||||||
|
deger = il;
|
||||||
|
} else {
|
||||||
|
deger = String(deger).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const profil = await profilAlanYaz(b.userId, alan, deger ?? null);
|
||||||
|
b.profil = profil;
|
||||||
|
const satirlar = profilSatirlari(profil);
|
||||||
|
return {
|
||||||
|
kaydedildi: deger == null ? `${alan} silindi` : satirlar.find((s) => s.alan === alan)?.metin,
|
||||||
|
profil: satirlar.map((s) => s.metin),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { hata: `Değer kabul edilmedi (${alan}); aralık dışı ya da geçersiz seçenek.` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Route'un araç döngüsüne verdiği çalıştırıcı. Bilinmeyen araç hata döner. */
|
||||||
|
export function aracCalistirici(b: AracBaglami) {
|
||||||
|
return async (ad: string, arg: unknown): Promise<unknown> => {
|
||||||
|
const a = (arg && typeof arg === "object" ? arg : {}) as Record<string, unknown>;
|
||||||
|
switch (ad) {
|
||||||
|
case "program_ara":
|
||||||
|
return programAra(b, a as ProgramAraArg);
|
||||||
|
case "program_detay":
|
||||||
|
return programDetay(b, a);
|
||||||
|
case "universite_profili":
|
||||||
|
return universiteProfili(b, a);
|
||||||
|
case "profil_guncelle":
|
||||||
|
return profilGuncelle(b, a);
|
||||||
|
default:
|
||||||
|
return { hata: `Bilinmeyen araç: ${ad}` };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { profilGetir };
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type OpenAI from "openai";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getOpenRouter, raporModeli, sohbetModeli } from "./client";
|
import { getOpenRouter, raporModeli, sohbetModeli } from "./client";
|
||||||
|
|
||||||
@@ -166,3 +167,109 @@ export async function* sohbetAkisi(opts: {
|
|||||||
// sanılmaması ve iade yolunun çalışması için zaman aşımını elle fırlat.
|
// sanılmaması ve iade yolunun çalışması için zaman aşımını elle fırlat.
|
||||||
if (sinyal.aborted) throw sinyal.reason;
|
if (sinyal.aborted) throw sinyal.reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Araçlı sohbet ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type AracCagrisiBirikim = { id: string; name: string; arguments: string };
|
||||||
|
|
||||||
|
const ARAC_TUR_SINIRI = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Araç çağrılı sohbet akışı. Model araç isterse (stream'de tool_calls
|
||||||
|
* parçaları birikir) araçlar çalıştırılır, sonuçlar `tool` mesajı olarak
|
||||||
|
* eklenir ve akış yeniden başlar; en fazla ARAC_TUR_SINIRI tur, son turda
|
||||||
|
* araç kapalı (cevap vermek zorunda). Metin parçaları geldikçe yield edilir.
|
||||||
|
*
|
||||||
|
* Sağlayıcı araçları desteklemiyorsa (ilk çağrıda 400/404) düz sohbete düşer.
|
||||||
|
*/
|
||||||
|
export async function* sohbetAkisiAraclarla(opts: {
|
||||||
|
system: string;
|
||||||
|
mesajlar: SohbetMesaji[];
|
||||||
|
maxTokens: number;
|
||||||
|
araclar: OpenAI.Chat.Completions.ChatCompletionTool[];
|
||||||
|
calistir: (ad: string, argumanlar: unknown) => Promise<unknown>;
|
||||||
|
/** Her araç çağrısında (günlük/analitik için) */
|
||||||
|
onArac?: (ad: string) => void;
|
||||||
|
}): AsyncGenerator<string> {
|
||||||
|
const sinyal = AbortSignal.timeout(120_000);
|
||||||
|
const gecmis: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||||
|
{ role: "system", content: opts.system },
|
||||||
|
...opts.mesajlar,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let tur = 0; tur < ARAC_TUR_SINIRI; tur++) {
|
||||||
|
const sonTur = tur === ARAC_TUR_SINIRI - 1;
|
||||||
|
let akis;
|
||||||
|
try {
|
||||||
|
akis = await geciciHatadaTekrarla(() =>
|
||||||
|
getOpenRouter().chat.completions.create(
|
||||||
|
{
|
||||||
|
model: sohbetModeli(),
|
||||||
|
max_tokens: opts.maxTokens,
|
||||||
|
reasoning_effort: "none",
|
||||||
|
stream: true,
|
||||||
|
messages: gecmis,
|
||||||
|
tools: opts.araclar,
|
||||||
|
tool_choice: sonTur ? "none" : "auto",
|
||||||
|
},
|
||||||
|
{ signal: sinyal },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (hata) {
|
||||||
|
const kod = (hata as { status?: number })?.status;
|
||||||
|
if (tur === 0 && (kod === 400 || kod === 404)) {
|
||||||
|
console.warn("[cagri] sağlayıcı araç çağrısını kabul etmedi, düz sohbete düşüldü:", kod);
|
||||||
|
yield* sohbetAkisi({ system: opts.system, mesajlar: opts.mesajlar, maxTokens: opts.maxTokens });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw hata;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metin = "";
|
||||||
|
const cagrilar = new Map<number, AracCagrisiBirikim>();
|
||||||
|
for await (const parca of akis) {
|
||||||
|
const delta = parca.choices?.[0]?.delta;
|
||||||
|
if (!delta) continue;
|
||||||
|
if (delta.content) {
|
||||||
|
metin += delta.content;
|
||||||
|
yield delta.content;
|
||||||
|
}
|
||||||
|
for (const tc of delta.tool_calls ?? []) {
|
||||||
|
const mevcut = cagrilar.get(tc.index) ?? { id: "", name: "", arguments: "" };
|
||||||
|
if (tc.id) mevcut.id = tc.id;
|
||||||
|
if (tc.function?.name) mevcut.name += tc.function.name;
|
||||||
|
if (tc.function?.arguments) mevcut.arguments += tc.function.arguments;
|
||||||
|
cagrilar.set(tc.index, mevcut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sinyal.aborted) throw sinyal.reason;
|
||||||
|
if (cagrilar.size === 0) return;
|
||||||
|
|
||||||
|
const liste = [...cagrilar.values()].filter((c) => c.name);
|
||||||
|
const toolCalls = liste.map((c) => ({
|
||||||
|
id: c.id || `call_${Math.random().toString(36).slice(2, 10)}`,
|
||||||
|
type: "function" as const,
|
||||||
|
function: { name: c.name, arguments: c.arguments || "{}" },
|
||||||
|
}));
|
||||||
|
gecmis.push({ role: "assistant", content: metin || null, tool_calls: toolCalls });
|
||||||
|
for (const [i, c] of liste.entries()) {
|
||||||
|
opts.onArac?.(c.name);
|
||||||
|
let arg: unknown = {};
|
||||||
|
try {
|
||||||
|
arg = c.arguments ? JSON.parse(c.arguments) : {};
|
||||||
|
} catch {
|
||||||
|
arg = {};
|
||||||
|
}
|
||||||
|
let sonuc: unknown;
|
||||||
|
try {
|
||||||
|
sonuc = await opts.calistir(c.name, arg);
|
||||||
|
} catch (hata) {
|
||||||
|
console.error("[cagri] araç hatası:", c.name, hata);
|
||||||
|
sonuc = { hata: "Araç çalışmadı; veriye ulaşılamadı, uydurma." };
|
||||||
|
}
|
||||||
|
gecmis.push({ role: "tool", tool_call_id: toolCalls[i].id, content: JSON.stringify(sonuc) });
|
||||||
|
}
|
||||||
|
// Araç turundan sonra araya boşluk: metin + tool ile bitmiş cümle yapışmasın
|
||||||
|
if (metin && !metin.endsWith("\n")) yield "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
|
|
||||||
// Tüm AI akışı OpenRouter üzerinden tek kapıdan gider (kullanıcı kararı,
|
// Tüm AI akışı OpenAI uyumlu TEK kapıdan gider. Varsayılan OpenRouter
|
||||||
// 7 Ağu 2026 — önceki Anthropic/Gemini/lokal sağlayıcı seçimi kaldırıldı).
|
// (7 Ağu 2026 kararı); 26 Eyl 2026 kararıyla model Türkiye'de barındırılacak:
|
||||||
// Model .env'den değiştirilebilir; rapor ve sohbet istenirse ayrışabilir.
|
// sağlayıcı değişince kod değil .env değişir — AI_BASE_URL + AI_API_KEY
|
||||||
|
// (OpenRouter için eski OPENROUTER_* adları da çalışır). Sağlayıcı değişirse
|
||||||
|
// /gizlilik "Hizmet sağlayıcılar" bölümü aynı commit'te güncellenir.
|
||||||
const VARSAYILAN_MODEL = "deepseek/deepseek-v4-flash";
|
const VARSAYILAN_MODEL = "deepseek/deepseek-v4-flash";
|
||||||
|
const VARSAYILAN_BASE_URL = "https://openrouter.ai/api/v1";
|
||||||
|
|
||||||
|
function baseUrl(): string {
|
||||||
|
return process.env.AI_BASE_URL?.trim() || VARSAYILAN_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiKey(): string | undefined {
|
||||||
|
return process.env.AI_API_KEY?.trim() || process.env.OPENROUTER_API_KEY?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OpenRouter dışı bir sağlayıcı tanımlı mı (gizlilik metni ve log için). */
|
||||||
|
export function saglayiciAdi(): string {
|
||||||
|
try {
|
||||||
|
return new URL(baseUrl()).hostname;
|
||||||
|
} catch {
|
||||||
|
return "bilinmiyor";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function raporModeli(): string {
|
export function raporModeli(): string {
|
||||||
return (
|
return (
|
||||||
@@ -23,20 +43,20 @@ export function sohbetModeli(): string {
|
|||||||
|
|
||||||
/** API anahtarı tanımlı mı (mock moduna düşülecek mi)? */
|
/** API anahtarı tanımlı mı (mock moduna düşülecek mi)? */
|
||||||
export function anahtarVar(): boolean {
|
export function anahtarVar(): boolean {
|
||||||
return Boolean(process.env.OPENROUTER_API_KEY);
|
return Boolean(apiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalForAi = globalThis as unknown as { __openrouter?: OpenAI };
|
const globalForAi = globalThis as unknown as { __openrouter?: OpenAI };
|
||||||
|
|
||||||
export function getOpenRouter(): OpenAI {
|
export function getOpenRouter(): OpenAI {
|
||||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
const key = apiKey();
|
||||||
if (!apiKey) {
|
if (!key) {
|
||||||
throw new Error("AI_KEY_MISSING");
|
throw new Error("AI_KEY_MISSING");
|
||||||
}
|
}
|
||||||
if (!globalForAi.__openrouter) {
|
if (!globalForAi.__openrouter) {
|
||||||
globalForAi.__openrouter = new OpenAI({
|
globalForAi.__openrouter = new OpenAI({
|
||||||
baseURL: "https://openrouter.ai/api/v1",
|
baseURL: baseUrl(),
|
||||||
apiKey,
|
apiKey: key,
|
||||||
// Tek retry otoritesi cagri.ts'teki geciciHatadaTekrarla
|
// Tek retry otoritesi cagri.ts'teki geciciHatadaTekrarla
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
// OpenRouter panelindeki kullanım dökümünde uygulama atfı (opsiyonel)
|
// OpenRouter panelindeki kullanım dökümünde uygulama atfı (opsiyonel)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { profilSatirlari, type DanismanProfili } from "@/lib/danisman-profil";
|
||||||
import "server-only";
|
import "server-only";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
@@ -231,8 +232,10 @@ export async function raporUret(
|
|||||||
params: RaporParams,
|
params: RaporParams,
|
||||||
revizyonNotu?: string,
|
revizyonNotu?: string,
|
||||||
oncekiListeOzeti?: string,
|
oncekiListeOzeti?: string,
|
||||||
|
profil?: DanismanProfili,
|
||||||
): Promise<RaporSonuc> {
|
): Promise<RaporSonuc> {
|
||||||
const { havuz, gevsetildi } = havuzOlustur(params);
|
const { havuz, gevsetildi } = havuzOlustur(params, profil);
|
||||||
|
const profilSatir = profil ? profilSatirlari(profil) : [];
|
||||||
if (havuz.length < MIN_LISTE) {
|
if (havuz.length < MIN_LISTE) {
|
||||||
throw new RaporUretimHatasi(
|
throw new RaporUretimHatasi(
|
||||||
"Bu sıralama ve filtrelerle yeterli program bulunamadı. Daha geniş kategori veya il seçmeyi dene.",
|
"Bu sıralama ve filtrelerle yeterli program bulunamadı. Daha geniş kategori veya il seçmeyi dene.",
|
||||||
@@ -252,6 +255,11 @@ export async function raporUret(
|
|||||||
(params.universiteTipi && params.universiteTipi !== "farketmez");
|
(params.universiteTipi && params.universiteTipi !== "farketmez");
|
||||||
|
|
||||||
const gevsetmeNotu: string[] = [];
|
const gevsetmeNotu: string[] = [];
|
||||||
|
if (gevsetildi.includes("profil")) {
|
||||||
|
gevsetmeNotu.push(
|
||||||
|
"Adayın danışmana söylediği tercihlerle (aşağıda) yeterli program yoktu; havuz bu tercihler uygulanmadan kuruldu. Uyanları öne al ve uyarılarda bunu tek cümleyle söyle.",
|
||||||
|
);
|
||||||
|
}
|
||||||
if (gevsetildi.includes("il")) {
|
if (gevsetildi.includes("il")) {
|
||||||
gevsetmeNotu.push(
|
gevsetmeNotu.push(
|
||||||
`Adayın seçtiği illerde yeterli program yoktu; havuza başka illerden de program eklendi. Seçili illere öncelik ver ama listeyi ${toplamHedef}'e tamamlamak için diğerlerini de kullan.`,
|
`Adayın seçtiği illerde yeterli program yoktu; havuza başka illerden de program eklendi. Seçili illere öncelik ver ama listeyi ${toplamHedef}'e tamamlamak için diğerlerini de kullan.`,
|
||||||
@@ -280,6 +288,9 @@ export async function raporUret(
|
|||||||
})}\nSeçili kategori ve illere öncelik ver; havuzun elverdiği ölçüde bu seçimlere sadık kal.`
|
})}\nSeçili kategori ve illere öncelik ver; havuzun elverdiği ölçüde bu seçimlere sadık kal.`
|
||||||
: null,
|
: null,
|
||||||
params.notlar ? `Adayın ek notları: ${params.notlar}` : null,
|
params.notlar ? `Adayın ek notları: ${params.notlar}` : null,
|
||||||
|
profilSatir.length > 0
|
||||||
|
? `ADAYIN DANIŞMANA SÖYLEDİĞİ TERCİHLER (havuz buna göre süzüldü; gerekçelerde uyanı belirt):\n${profilSatir.map((s) => `- ${s.metin}`).join("\n")}`
|
||||||
|
: null,
|
||||||
gevsetmeNotu.length > 0 ? gevsetmeNotu.join("\n") : null,
|
gevsetmeNotu.length > 0 ? gevsetmeNotu.join("\n") : null,
|
||||||
revizyonNotu
|
revizyonNotu
|
||||||
? `REVİZYON İSTEĞİ: Aday mevcut listede şu değişikliği istiyor: ${revizyonNotu}\nÖnceki listenin özeti: ${oncekiListeOzeti ?? "yok"}`
|
? `REVİZYON İSTEĞİ: Aday mevcut listede şu değişikliği istiyor: ${revizyonNotu}\nÖnceki listenin özeti: ${oncekiListeOzeti ?? "yok"}`
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ export type OlayAdi =
|
|||||||
// kapi_kapatildi {adim, tetik}: yalnız TAMAMLANMADAN kapanış (X, dışarı
|
// kapi_kapatildi {adim, tetik}: yalnız TAMAMLANMADAN kapanış (X, dışarı
|
||||||
// tıklama, Vazgeç); tamamlanan kapıda atılmaz.
|
// tıklama, Vazgeç); tamamlanan kapıda atılmaz.
|
||||||
| "kapi_acildi"
|
| "kapi_acildi"
|
||||||
| "kapi_kapatildi";
|
| "kapi_kapatildi"
|
||||||
|
// Danışman profili: yalnız ALAN ADI gider, değer gitmez (KVKK: analitiğe
|
||||||
|
// profil değeri sızmaz). kaydedildi = model aracı çağırıp alanı yazdı
|
||||||
|
// (istemci cevap sonrası profili çekip farkı görür); silindi = çipteki çarpı.
|
||||||
|
| "profil_alani_kaydedildi"
|
||||||
|
| "profil_alani_silindi";
|
||||||
|
|
||||||
export type Ozellikler = Record<string, string | number>;
|
export type Ozellikler = Record<string, string | number>;
|
||||||
|
|
||||||
|
|||||||
@@ -231,3 +231,15 @@ export const savedLists = sqliteTable("saved_lists", {
|
|||||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Danışman profili: sohbetten `profil_guncelle` aracıyla çıkarılan, KAPALI
|
||||||
|
// LİSTELİ tercih alanları (bkz. src/lib/danisman-profil.ts). Serbest metin
|
||||||
|
// yok; öğrenci /listem'de görür ve alan alan siler. Kullanıcı başına tek satır.
|
||||||
|
export const danismanProfil = sqliteTable("danisman_profil", {
|
||||||
|
userId: text("user_id")
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => user.id),
|
||||||
|
// DanismanProfili (yalnız tanımlı anahtarlar; okurken yeniden doğrulanır)
|
||||||
|
alanlar: text("alanlar", { mode: "json" }).notNull(),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -137,6 +137,33 @@ export async function spendCreditForMessage(opts: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kredi DÜŞMEDEN kullanıcı mesajı kaydeder — yalnız profil onayı ("kaydedeyim
|
||||||
|
* mi?" → "evet") için (Bilal kararı, 26 Eyl 2026: onay ücretsiz). Ledger'a
|
||||||
|
* satır yazılmaz; clientMessageId çakışırsa DUPLICATE.
|
||||||
|
*/
|
||||||
|
export async function bedelsizMesajKaydet(opts: {
|
||||||
|
userId: string;
|
||||||
|
content: string;
|
||||||
|
clientMessageId: string;
|
||||||
|
}): Promise<SpendResult & { messageId?: string }> {
|
||||||
|
const messageId = newId();
|
||||||
|
try {
|
||||||
|
await appDb.insert(chatMessages).values({
|
||||||
|
id: messageId,
|
||||||
|
userId: opts.userId,
|
||||||
|
role: "user",
|
||||||
|
content: opts.content,
|
||||||
|
clientMessageId: opts.clientMessageId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
return { ok: true, messageId };
|
||||||
|
} catch (err) {
|
||||||
|
if (uniqueIhlaliMi(err)) return { ok: false, error: "DUPLICATE" };
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** `paketTekil` ile çağrılan tanımlamada kullanıcı zaten paketliyse fırlatılır. */
|
/** `paketTekil` ile çağrılan tanımlamada kullanıcı zaten paketliyse fırlatılır. */
|
||||||
class ZatenPaketli extends Error {}
|
class ZatenPaketli extends Error {}
|
||||||
|
|
||||||
|
|||||||
56
src/lib/danisman-profil-db.ts
Normal file
56
src/lib/danisman-profil-db.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import "server-only";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { appDb, schema } from "@/lib/appdb";
|
||||||
|
import {
|
||||||
|
PROFIL_SEMASI,
|
||||||
|
profilTemizle,
|
||||||
|
type DanismanProfili,
|
||||||
|
type ProfilAlani,
|
||||||
|
} from "@/lib/danisman-profil";
|
||||||
|
|
||||||
|
/** Kullanıcının danışman profili; satır yoksa boş nesne. Bozuk alanlar düşer. */
|
||||||
|
export async function profilGetir(userId: string): Promise<DanismanProfili> {
|
||||||
|
const satir = await appDb.query.danismanProfil.findFirst({
|
||||||
|
where: eq(schema.danismanProfil.userId, userId),
|
||||||
|
columns: { alanlar: true },
|
||||||
|
});
|
||||||
|
return profilTemizle(satir?.alanlar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tek alanı yazar (deger null → alan silinir). Değer şemadan geçmezse hata
|
||||||
|
* fırlatır; çağıran taraf (araç) bunu modele "kaydedilmedi" diye döner.
|
||||||
|
*/
|
||||||
|
export async function profilAlanYaz(
|
||||||
|
userId: string,
|
||||||
|
alan: ProfilAlani,
|
||||||
|
deger: unknown,
|
||||||
|
): Promise<DanismanProfili> {
|
||||||
|
const mevcut = await profilGetir(userId);
|
||||||
|
const yeni: Record<string, unknown> = { ...mevcut };
|
||||||
|
if (deger === null || deger === undefined) {
|
||||||
|
delete yeni[alan];
|
||||||
|
} else {
|
||||||
|
const dogrulama = PROFIL_SEMASI.shape[alan].safeParse(deger);
|
||||||
|
if (!dogrulama.success) {
|
||||||
|
throw new Error(`Geçersiz değer: ${alan}`);
|
||||||
|
}
|
||||||
|
yeni[alan] = dogrulama.data;
|
||||||
|
}
|
||||||
|
const alanlar = PROFIL_SEMASI.parse(yeni);
|
||||||
|
const now = new Date();
|
||||||
|
await appDb
|
||||||
|
.insert(schema.danismanProfil)
|
||||||
|
.values({ userId, alanlar, updatedAt: now })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.danismanProfil.userId,
|
||||||
|
set: { alanlar, updatedAt: now },
|
||||||
|
});
|
||||||
|
return alanlar;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function profilSil(userId: string): Promise<void> {
|
||||||
|
await appDb
|
||||||
|
.delete(schema.danismanProfil)
|
||||||
|
.where(eq(schema.danismanProfil.userId, userId));
|
||||||
|
}
|
||||||
119
src/lib/danisman-profil.ts
Normal file
119
src/lib/danisman-profil.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Danışman profili — sohbetten çıkarılan öğrenci tercihleri. KAPALI LİSTE:
|
||||||
|
* yalnız burada tanımlı alanlar yazılabilir, serbest metin alanı yoktur.
|
||||||
|
* Neden: model sohbetten din, sağlık, aile gibi özel nitelikli veriyi profile
|
||||||
|
* taşıyamasın (KVKK md. 6); öğrenci her alanı görür ve tek tıkla siler
|
||||||
|
* (docs/arastirma/2026-09-25-kisisellestirme/README.md §8).
|
||||||
|
*
|
||||||
|
* Sorulmayanlar: cinsiyet, sağlık/engel, gelir. Ücret üst sınırı sorulur, gelir
|
||||||
|
* sorulmaz. Konum il düzeyindedir (adres/koordinat değil).
|
||||||
|
*
|
||||||
|
* KULLANIM SINIRI (karar, 25 Eyl 2026): profil yalnız liste üretimi
|
||||||
|
* (rapor-havuzu) ve danışman aramalarında kullanılır; pazarlama, e-posta,
|
||||||
|
* segmentleme ya da analitik değerinde KULLANILMAZ — kullanılırsa açık rıza
|
||||||
|
* konusu olur (hukuk-ve-rakipler.md C.3/13). Profil boşken hizmet tam çalışır.
|
||||||
|
*
|
||||||
|
* Saf modül: client, server ve scripts aynı dosyayı okur.
|
||||||
|
*/
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const PROFIL_SECENEKLERI = {
|
||||||
|
burs: {
|
||||||
|
"yalniz-burslu": "yalnız burslu",
|
||||||
|
yuzde50: "%50 burslu da olur",
|
||||||
|
"ucretli-olur": "ücretli de olur",
|
||||||
|
},
|
||||||
|
dil: {
|
||||||
|
turkce: "Türkçe",
|
||||||
|
ingilizce: "İngilizce",
|
||||||
|
farketmez: "fark etmez",
|
||||||
|
},
|
||||||
|
ogretimTuru: {
|
||||||
|
"yalniz-orgun": "yalnız örgün",
|
||||||
|
"uzaktan-olur": "uzaktan/açık da olur",
|
||||||
|
},
|
||||||
|
kampus: {
|
||||||
|
sakin: "sakin, küçük",
|
||||||
|
kalabalik: "kalabalık, büyük",
|
||||||
|
farketmez: "fark etmez",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const PROFIL_SEMASI = z
|
||||||
|
.object({
|
||||||
|
/** Yaşadığı il — DB'deki il adı biçimi (büyük harf, Türkçe). */
|
||||||
|
evIl: z.string().min(2).max(40).optional(),
|
||||||
|
/** Evden en fazla kaç km (il merkezleri arası kuş uçuşu). */
|
||||||
|
maxKm: z.number().int().min(50).max(1500).optional(),
|
||||||
|
/** Vakıfta yıllık ücret üst sınırı (TL). */
|
||||||
|
ucretUst: z.number().int().min(50_000).max(5_000_000).optional(),
|
||||||
|
burs: z.enum(["yalniz-burslu", "yuzde50", "ucretli-olur"]).optional(),
|
||||||
|
dil: z.enum(["turkce", "ingilizce", "farketmez"]).optional(),
|
||||||
|
ogretimTuru: z.enum(["yalniz-orgun", "uzaktan-olur"]).optional(),
|
||||||
|
kampus: z.enum(["sakin", "kalabalik", "farketmez"]).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export type DanismanProfili = z.infer<typeof PROFIL_SEMASI>;
|
||||||
|
export type ProfilAlani = keyof DanismanProfili;
|
||||||
|
|
||||||
|
export const PROFIL_ALANLARI = Object.keys(PROFIL_SEMASI.shape) as ProfilAlani[];
|
||||||
|
|
||||||
|
/** Alanın öğrenciye görünen kısa açıklaması (çip ve araç şeması aynı metni kullanır). */
|
||||||
|
export const PROFIL_ALAN_ACIKLAMA: Record<ProfilAlani, string> = {
|
||||||
|
evIl: "Yaşadığı il (evden uzaklık için)",
|
||||||
|
maxKm: "Evden en fazla kaç km uzakta okumak istediği",
|
||||||
|
ucretUst: "Vakıfta yıllık ücret üst sınırı (TL)",
|
||||||
|
burs: "Vakıfta burs beklentisi",
|
||||||
|
dil: "Öğretim dili tercihi",
|
||||||
|
ogretimTuru: "Uzaktan/açık öğretim kabul mü",
|
||||||
|
kampus: "Kampüs büyüklüğü tercihi",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Bilinmeyen anahtarları atarak profili güvenli okur; bozuk değer alanı düşürür. */
|
||||||
|
export function profilTemizle(ham: unknown): DanismanProfili {
|
||||||
|
if (!ham || typeof ham !== "object") return {};
|
||||||
|
const sonuc: Record<string, unknown> = {};
|
||||||
|
for (const alan of PROFIL_ALANLARI) {
|
||||||
|
const deger = (ham as Record<string, unknown>)[alan];
|
||||||
|
if (deger === undefined || deger === null) continue;
|
||||||
|
const tek = PROFIL_SEMASI.shape[alan].safeParse(deger);
|
||||||
|
if (tek.success) sonuc[alan] = tek.data;
|
||||||
|
}
|
||||||
|
return sonuc as DanismanProfili;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tek alanı öğrencinin okuyacağı cümleye çevirir: "Yıllık ücret en fazla 300.000 TL". */
|
||||||
|
export function profilAlaniMetni(alan: ProfilAlani, deger: unknown): string {
|
||||||
|
switch (alan) {
|
||||||
|
case "evIl":
|
||||||
|
return `Yaşadığı il: ${ilBaslik(String(deger))}`;
|
||||||
|
case "maxKm":
|
||||||
|
return `Evden en fazla ${Number(deger).toLocaleString("tr-TR")} km`;
|
||||||
|
case "ucretUst":
|
||||||
|
return `Yıllık ücret en fazla ${Number(deger).toLocaleString("tr-TR")} TL`;
|
||||||
|
case "burs":
|
||||||
|
return `Vakıfta ${PROFIL_SECENEKLERI.burs[deger as keyof typeof PROFIL_SECENEKLERI.burs]}`;
|
||||||
|
case "dil":
|
||||||
|
return `Öğretim dili: ${PROFIL_SECENEKLERI.dil[deger as keyof typeof PROFIL_SECENEKLERI.dil]}`;
|
||||||
|
case "ogretimTuru":
|
||||||
|
return deger === "yalniz-orgun" ? "Yalnız örgün öğretim" : "Uzaktan/açık öğretim de olur";
|
||||||
|
case "kampus":
|
||||||
|
return `Kampüs: ${PROFIL_SECENEKLERI.kampus[deger as keyof typeof PROFIL_SECENEKLERI.kampus]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Profilin tamamı, dolu alanlar sırayla. */
|
||||||
|
export function profilSatirlari(p: DanismanProfili): { alan: ProfilAlani; metin: string }[] {
|
||||||
|
return PROFIL_ALANLARI.flatMap((alan) =>
|
||||||
|
p[alan] === undefined ? [] : [{ alan, metin: profilAlaniMetni(alan, p[alan]) }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ilBaslik(il: string): string {
|
||||||
|
return il
|
||||||
|
.toLocaleLowerCase("tr-TR")
|
||||||
|
.split(" ")
|
||||||
|
.map((k) => k.charAt(0).toLocaleUpperCase("tr-TR") + k.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
@@ -75,6 +75,48 @@ export function programSatiri(row: ProgramHamSatiri): Program {
|
|||||||
* sıralamasından küçükse geçen yıl oraya yerleşmek daha iyi dereceyle mümkündü
|
* sıralamasından küçükse geçen yıl oraya yerleşmek daha iyi dereceyle mümkündü
|
||||||
* demektir (hayal), büyükse daha güvenli demektir (garanti).
|
* demektir (hayal), büyükse daha güvenli demektir (garanti).
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Danışman profilinden gelen program özelliği süzgeci (program_ozellik
|
||||||
|
* tablosu; bkz. lib/program-ozellik.ts). Tablo yoksa süzgeç uygulanmaz.
|
||||||
|
* Devlet programlarında ücret/burs NULL'dur ve her zaman geçer.
|
||||||
|
*/
|
||||||
|
export type OzellikSuzgeci = {
|
||||||
|
dil?: "Türkçe" | "İngilizce";
|
||||||
|
yalnizOrgun?: boolean;
|
||||||
|
yalnizBurslu?: boolean;
|
||||||
|
ucretUst?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ozellikTablosuVar: boolean | null = null;
|
||||||
|
|
||||||
|
function ozellikFiltreSql(o?: OzellikSuzgeci): { sql: string; args: (string | number)[] } {
|
||||||
|
if (!o) return { sql: "", args: [] };
|
||||||
|
const kosul: string[] = [];
|
||||||
|
const args: (string | number)[] = [];
|
||||||
|
if (o.dil) {
|
||||||
|
kosul.push("ogrenim_dili LIKE ?");
|
||||||
|
args.push(`${o.dil}%`);
|
||||||
|
}
|
||||||
|
if (o.yalnizOrgun) kosul.push("(ogrenim_turu IS NULL OR ogrenim_turu = 'Örgün Öğretim')");
|
||||||
|
if (o.yalnizBurslu) kosul.push("(burs_orani IS NULL OR burs_orani = 'Burslu')");
|
||||||
|
if (o.ucretUst) {
|
||||||
|
kosul.push("(ucret IS NULL OR burs_orani = 'Burslu' OR ucret <= ?)");
|
||||||
|
args.push(o.ucretUst);
|
||||||
|
}
|
||||||
|
if (kosul.length === 0) return { sql: "", args: [] };
|
||||||
|
if (ozellikTablosuVar == null) {
|
||||||
|
ozellikTablosuVar =
|
||||||
|
getDb()
|
||||||
|
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='program_ozellik'")
|
||||||
|
.get() !== undefined;
|
||||||
|
}
|
||||||
|
if (!ozellikTablosuVar) return { sql: "", args: [] };
|
||||||
|
return {
|
||||||
|
sql: `AND id IN (SELECT id FROM program_ozellik WHERE ${kosul.join(" AND ")})`,
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function uniturFiltreSql(grup?: UniturGrubu): { sql: string; args: string[] } {
|
function uniturFiltreSql(grup?: UniturGrubu): { sql: string; args: string[] } {
|
||||||
if (grup === "devlet") return { sql: "AND unitur = ?", args: ["DEVLET"] };
|
if (grup === "devlet") return { sql: "AND unitur = ?", args: ["DEVLET"] };
|
||||||
if (grup === "vakif")
|
if (grup === "vakif")
|
||||||
@@ -112,6 +154,7 @@ export function dilimAra(
|
|||||||
il?: string;
|
il?: string;
|
||||||
iller?: string[];
|
iller?: string[];
|
||||||
uniturGrubu?: UniturGrubu;
|
uniturGrubu?: UniturGrubu;
|
||||||
|
ozellik?: OzellikSuzgeci;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
} = {}
|
} = {}
|
||||||
@@ -132,16 +175,17 @@ export function dilimAra(
|
|||||||
? `AND il IN (${illerListe.map(() => "?").join(",")})`
|
? `AND il IN (${illerListe.map(() => "?").join(",")})`
|
||||||
: "";
|
: "";
|
||||||
const unitur = uniturFiltreSql(opts.uniturGrubu);
|
const unitur = uniturFiltreSql(opts.uniturGrubu);
|
||||||
|
const ozellik = ozellikFiltreSql(opts.ozellik);
|
||||||
|
|
||||||
const { alt, ust } = dilimAraligi(sira, dilim);
|
const { alt, ust } = dilimAraligi(sira, dilim);
|
||||||
if (ust != null && ust < alt) return { programlar: [], toplam: 0 };
|
if (ust != null && ust < alt) return { programlar: [], toplam: 0 };
|
||||||
const siraKosulu =
|
const siraKosulu =
|
||||||
ust != null ? `${EFEKTIF_SIRA} BETWEEN ? AND ?` : `${EFEKTIF_SIRA} >= ?`;
|
ust != null ? `${EFEKTIF_SIRA} BETWEEN ? AND ?` : `${EFEKTIF_SIRA} >= ?`;
|
||||||
const siraArgs = ust != null ? [alt, ust] : [alt];
|
const siraArgs = ust != null ? [alt, ust] : [alt];
|
||||||
const args = [tur, onlisans, ...illerListe, ...unitur.args, ...siraArgs];
|
const args = [tur, onlisans, ...illerListe, ...unitur.args, ...ozellik.args, ...siraArgs];
|
||||||
|
|
||||||
const base = `FROM programs
|
const base = `FROM programs
|
||||||
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql}
|
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql} ${ozellik.sql}
|
||||||
AND ${EFEKTIF_SIRA} IS NOT NULL
|
AND ${EFEKTIF_SIRA} IS NOT NULL
|
||||||
AND ${siraKosulu}`;
|
AND ${siraKosulu}`;
|
||||||
// hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa
|
// hayal: sınıra en yakın (en büyük taban) önce; diğerleri en yakından uzağa
|
||||||
@@ -168,6 +212,7 @@ export function searchByRank(
|
|||||||
il?: string;
|
il?: string;
|
||||||
iller?: string[];
|
iller?: string[];
|
||||||
uniturGrubu?: UniturGrubu;
|
uniturGrubu?: UniturGrubu;
|
||||||
|
ozellik?: OzellikSuzgeci;
|
||||||
limitPerBucket?: number;
|
limitPerBucket?: number;
|
||||||
} = {}
|
} = {}
|
||||||
): RankResults {
|
): RankResults {
|
||||||
@@ -175,6 +220,7 @@ export function searchByRank(
|
|||||||
il: opts.il,
|
il: opts.il,
|
||||||
iller: opts.iller,
|
iller: opts.iller,
|
||||||
uniturGrubu: opts.uniturGrubu,
|
uniturGrubu: opts.uniturGrubu,
|
||||||
|
ozellik: opts.ozellik,
|
||||||
limit: opts.limitPerBucket ?? 12,
|
limit: opts.limitPerBucket ?? 12,
|
||||||
};
|
};
|
||||||
const hayal = dilimAra(sira, turKey, "hayal", ortak);
|
const hayal = dilimAra(sira, turKey, "hayal", ortak);
|
||||||
|
|||||||
42
src/lib/mesafe.ts
Normal file
42
src/lib/mesafe.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// İl merkezleri arası kuş uçuşu mesafe (km). Profildeki "evden en fazla N km"
|
||||||
|
// filtresi il düzeyinde çalışır: adres/koordinat tutulmaz (KVKK, §8).
|
||||||
|
import { IL_MERKEZ, type LatLng } from "./uni-koordinatlar";
|
||||||
|
|
||||||
|
const DUNYA_YARICAP_KM = 6371;
|
||||||
|
|
||||||
|
export function kmMesafe(a: LatLng, b: LatLng): number {
|
||||||
|
const rad = (d: number) => (d * Math.PI) / 180;
|
||||||
|
const dLat = rad(b[0] - a[0]);
|
||||||
|
const dLng = rad(b[1] - a[1]);
|
||||||
|
const h =
|
||||||
|
Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(rad(a[0])) * Math.cos(rad(b[0])) * Math.sin(dLng / 2) ** 2;
|
||||||
|
return Math.round(2 * DUNYA_YARICAP_KM * Math.asin(Math.sqrt(h)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DB il adı biçimine (büyük harf, Türkçe) çevirir; bilinmiyorsa null. */
|
||||||
|
export function ilAdiNormalize(ham: string): string | null {
|
||||||
|
const aday = ham.trim().toLocaleUpperCase("tr-TR");
|
||||||
|
if (aday in IL_MERKEZ) return aday;
|
||||||
|
// "Afyon" → AFYONKARAHİSAR gibi öneki eşleşen tek il varsa onu al
|
||||||
|
const adaylar = Object.keys(IL_MERKEZ).filter((il) => il.startsWith(aday));
|
||||||
|
return adaylar.length === 1 ? adaylar[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ev iline en fazla `km` uzaktaki iller (evin kendisi dahil), yakından uzağa. */
|
||||||
|
export function yakinIller(evIl: string, km: number): string[] {
|
||||||
|
const ev = IL_MERKEZ[evIl];
|
||||||
|
if (!ev) return [];
|
||||||
|
return Object.entries(IL_MERKEZ)
|
||||||
|
.map(([il, k]) => ({ il, km: kmMesafe(ev, k) }))
|
||||||
|
.filter((x) => x.km <= km)
|
||||||
|
.sort((a, b) => a.km - b.km)
|
||||||
|
.map((x) => x.il);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** İki il arası km; biri bilinmiyorsa null. */
|
||||||
|
export function illerArasiKm(a: string, b: string): number | null {
|
||||||
|
const ka = IL_MERKEZ[a];
|
||||||
|
const kb = IL_MERKEZ[b];
|
||||||
|
return ka && kb ? kmMesafe(ka, kb) : null;
|
||||||
|
}
|
||||||
137
src/lib/program-ozellik.ts
Normal file
137
src/lib/program-ozellik.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Program özellikleri: YÖK Atlas API'sinden scripts/program-ozellik.ts ile
|
||||||
|
// `program_ozellik` ve `kosul` tablolarına yazılan, yıl anlamı olmayan alanlar
|
||||||
|
// (ücret, burs, dil, öğretim türü, akreditasyon, kadro, baraj, ilçe, koşullar).
|
||||||
|
// Sıra/puan/kontenjan gibi yıllık alanlar burada DEĞİL (bkz. db.ts).
|
||||||
|
import { getDb } from "./db";
|
||||||
|
|
||||||
|
export type ProgramOzellik = {
|
||||||
|
id: string;
|
||||||
|
/** Vakıf/KKTC yıllık öğrenim ücreti (TL, KDV dahil, kılavuz yılı); devlette null */
|
||||||
|
ucret: number | null;
|
||||||
|
/** "Burslu" | "%50 İndirimli" | "%25 İndirimli" | "Ücretli"; devlette null */
|
||||||
|
bursOrani: string | null;
|
||||||
|
/** "Türkçe" | "İngilizce" | "İngilizce (%30)" | "Almanca" … */
|
||||||
|
ogrenimDili: string | null;
|
||||||
|
/** "Örgün Öğretim" | "Uzaktan Öğretim" | "Açık Öğretim" | "UOLP" */
|
||||||
|
ogrenimTuru: string | null;
|
||||||
|
/** Program akreditasyonu (MÜDEK, TEPDAD, FEDEK …) */
|
||||||
|
akreditasyon: string | null;
|
||||||
|
/** Üniversite akreditasyonu (YÖKAK …) */
|
||||||
|
uniAkreditasyon: string | null;
|
||||||
|
prof: number | null;
|
||||||
|
doc: number | null;
|
||||||
|
dou: number | null;
|
||||||
|
argor: number | null;
|
||||||
|
/** Tercih edebilmek için gereken en düşük başarı sırası (baraj) */
|
||||||
|
minBasariSirasi: number | null;
|
||||||
|
/** Kampüs ilçesi (büyük harf, API'den) */
|
||||||
|
ilce: string | null;
|
||||||
|
/** Yerleşme koşulu kodları (kosul tablosundan metne çevrilir) */
|
||||||
|
kosulKodlari: string[];
|
||||||
|
/** Kılavuz yılı */
|
||||||
|
yil: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HamSatir = {
|
||||||
|
id: string;
|
||||||
|
ucret: number | null;
|
||||||
|
burs_orani: string | null;
|
||||||
|
ogrenim_dili: string | null;
|
||||||
|
ogrenim_turu: string | null;
|
||||||
|
akreditasyon: string | null;
|
||||||
|
uni_akreditasyon: string | null;
|
||||||
|
prof: number | null;
|
||||||
|
doc: number | null;
|
||||||
|
dou: number | null;
|
||||||
|
argor: number | null;
|
||||||
|
min_basari_sirasi: number | null;
|
||||||
|
ilce: string | null;
|
||||||
|
kosul_kodlari: string | null;
|
||||||
|
yil: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OZELLIK_COLS = `id, ucret, burs_orani, ogrenim_dili, ogrenim_turu, akreditasyon,
|
||||||
|
uni_akreditasyon, prof, doc, dou, argor, min_basari_sirasi, ilce, kosul_kodlari, yil`;
|
||||||
|
|
||||||
|
function satir(r: HamSatir): ProgramOzellik {
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
ucret: r.ucret,
|
||||||
|
bursOrani: r.burs_orani,
|
||||||
|
ogrenimDili: r.ogrenim_dili,
|
||||||
|
ogrenimTuru: r.ogrenim_turu,
|
||||||
|
akreditasyon: r.akreditasyon,
|
||||||
|
uniAkreditasyon: r.uni_akreditasyon,
|
||||||
|
prof: r.prof,
|
||||||
|
doc: r.doc,
|
||||||
|
dou: r.dou,
|
||||||
|
argor: r.argor,
|
||||||
|
minBasariSirasi: r.min_basari_sirasi,
|
||||||
|
ilce: r.ilce,
|
||||||
|
kosulKodlari: r.kosul_kodlari ? r.kosul_kodlari.split(",") : [],
|
||||||
|
yil: r.yil,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tablo yoksa (eski DB) her okuma null/boş döner; sayfa kırılmaz. */
|
||||||
|
function tabloVar(): boolean {
|
||||||
|
return (
|
||||||
|
getDb()
|
||||||
|
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='program_ozellik'")
|
||||||
|
.get() !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function programOzellikGetir(id: string): ProgramOzellik | null {
|
||||||
|
if (!tabloVar()) return null;
|
||||||
|
const r = getDb()
|
||||||
|
.prepare(`SELECT ${OZELLIK_COLS} FROM program_ozellik WHERE id = ?`)
|
||||||
|
.get(id) as HamSatir | undefined;
|
||||||
|
return r ? satir(r) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function programOzellikleriGetir(
|
||||||
|
ids: readonly string[]
|
||||||
|
): Record<string, ProgramOzellik> {
|
||||||
|
const sonuc: Record<string, ProgramOzellik> = {};
|
||||||
|
if (ids.length === 0 || !tabloVar()) return sonuc;
|
||||||
|
const satirlar = getDb()
|
||||||
|
.prepare(
|
||||||
|
`SELECT ${OZELLIK_COLS} FROM program_ozellik WHERE id IN (${ids.map(() => "?").join(",")})`
|
||||||
|
)
|
||||||
|
.all(...ids) as HamSatir[];
|
||||||
|
for (const r of satirlar) sonuc[r.id] = satir(r);
|
||||||
|
return sonuc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Koşul kodlarının kılavuz metinleri, kod sırasıyla. Bilinmeyen kod atlanır. */
|
||||||
|
export function kosulMetinleri(kodlar: readonly string[]): { kod: string; metin: string }[] {
|
||||||
|
if (kodlar.length === 0 || !tabloVar()) return [];
|
||||||
|
const satirlar = getDb()
|
||||||
|
.prepare(`SELECT kod, metin FROM kosul WHERE kod IN (${kodlar.map(() => "?").join(",")})`)
|
||||||
|
.all(...kodlar) as { kod: string; metin: string }[];
|
||||||
|
const harita = new Map(satirlar.map((s) => [s.kod, s.metin]));
|
||||||
|
return kodlar.flatMap((k) => (harita.has(k) ? [{ kod: k, metin: harita.get(k)! }] : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kadro toplamı (prof + doç + dr. öğr. üyesi); hiçbiri yoksa null. */
|
||||||
|
export function ogretimUyesiToplami(o: ProgramOzellik): number | null {
|
||||||
|
const parcalar = [o.prof, o.doc, o.dou].filter((x): x is number => x != null);
|
||||||
|
return parcalar.length ? parcalar.reduce((a, b) => a + b, 0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Öğrenciye okunan kısa özet: "İngilizce · Burslu · MÜDEK akreditasyonlu". */
|
||||||
|
export function ozellikOzeti(o: ProgramOzellik): string[] {
|
||||||
|
const parcalar: string[] = [];
|
||||||
|
if (o.ogrenimDili && o.ogrenimDili !== "Türkçe") parcalar.push(o.ogrenimDili);
|
||||||
|
if (o.ogrenimTuru && o.ogrenimTuru !== "Örgün Öğretim") parcalar.push(o.ogrenimTuru);
|
||||||
|
if (o.bursOrani) parcalar.push(o.bursOrani);
|
||||||
|
if (o.ucret != null && o.bursOrani !== "Burslu") {
|
||||||
|
parcalar.push(`yıllık ${o.ucret.toLocaleString("tr-TR")} TL`);
|
||||||
|
}
|
||||||
|
if (o.akreditasyon) parcalar.push(`${o.akreditasyon} akreditasyonlu`);
|
||||||
|
if (o.minBasariSirasi != null) {
|
||||||
|
parcalar.push(`baraj ${o.minBasariSirasi.toLocaleString("tr-TR")}. sıra`);
|
||||||
|
}
|
||||||
|
return parcalar;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
// server-only guard'ı bilinçli yok: scripts/tadimlik-uret.ts bu modülü düz
|
// server-only guard'ı bilinçli yok: scripts/tadimlik-uret.ts bu modülü düz
|
||||||
// node (tsx) altında import ediyor; server-only paketi orada fırlar.
|
// node (tsx) altında import ediyor; server-only paketi orada fırlar.
|
||||||
import { searchByRank } from "./db";
|
import { searchByRank, type OzellikSuzgeci } from "./db";
|
||||||
|
import type { DanismanProfili } from "./danisman-profil";
|
||||||
|
import { yakinIller } from "./mesafe";
|
||||||
import {
|
import {
|
||||||
PUAN_TURLERI,
|
PUAN_TURLERI,
|
||||||
type Program,
|
type Program,
|
||||||
@@ -16,10 +18,55 @@ export interface AdayProgram extends Program {
|
|||||||
dilim: Dilim;
|
dilim: Dilim;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Gevsetilen = "profil" | "kategori" | "il";
|
||||||
|
|
||||||
export interface HavuzSonuc {
|
export interface HavuzSonuc {
|
||||||
havuz: AdayProgram[];
|
havuz: AdayProgram[];
|
||||||
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi
|
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi (sırayla)
|
||||||
gevsetildi: ("kategori" | "il")[];
|
gevsetildi: Gevsetilen[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Danışman profilinden sert süzgeç: dil, öğretim türü, burs, ücret üst sınırı
|
||||||
|
* (program_ozellik) ve ev iline km sınırı (il listesi). Sihirbazda il
|
||||||
|
* seçilmişse km sınırı uygulanmaz — sihirbaz seçimi daha yeni ve açık.
|
||||||
|
*/
|
||||||
|
export function profilSuzgeci(
|
||||||
|
profil: DanismanProfili | undefined,
|
||||||
|
sihirbazIlVar: boolean,
|
||||||
|
): { ozellik?: OzellikSuzgeci; iller?: string[]; ozet: string[] } {
|
||||||
|
if (!profil) return { ozet: [] };
|
||||||
|
const ozellik: OzellikSuzgeci = {};
|
||||||
|
const ozet: string[] = [];
|
||||||
|
if (profil.dil === "ingilizce" || profil.dil === "turkce") {
|
||||||
|
ozellik.dil = profil.dil === "ingilizce" ? "İngilizce" : "Türkçe";
|
||||||
|
ozet.push(`öğretim dili ${ozellik.dil}`);
|
||||||
|
}
|
||||||
|
if (profil.ogretimTuru === "yalniz-orgun") {
|
||||||
|
ozellik.yalnizOrgun = true;
|
||||||
|
ozet.push("yalnız örgün öğretim");
|
||||||
|
}
|
||||||
|
if (profil.burs === "yalniz-burslu") {
|
||||||
|
ozellik.yalnizBurslu = true;
|
||||||
|
ozet.push("vakıfta yalnız burslu");
|
||||||
|
}
|
||||||
|
if (profil.ucretUst) {
|
||||||
|
ozellik.ucretUst = profil.ucretUst;
|
||||||
|
ozet.push(`yıllık ücret en fazla ${profil.ucretUst.toLocaleString("tr-TR")} TL`);
|
||||||
|
}
|
||||||
|
let iller: string[] | undefined;
|
||||||
|
if (!sihirbazIlVar && profil.evIl && profil.maxKm) {
|
||||||
|
const yakin = yakinIller(profil.evIl, profil.maxKm);
|
||||||
|
if (yakin.length > 0) {
|
||||||
|
iller = yakin;
|
||||||
|
ozet.push(`${profil.evIl} iline en fazla ${profil.maxKm} km`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ozellik: Object.keys(ozellik).length ? ozellik : undefined,
|
||||||
|
iller,
|
||||||
|
ozet,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const HEDEF = 24;
|
const HEDEF = 24;
|
||||||
@@ -89,12 +136,16 @@ function dilimEkle(r: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministik aday havuzu: Claude bu havuzdan SEÇER, asla uydurmaz.
|
* Deterministik aday havuzu: yapay zekâ bu havuzdan SEÇER, asla uydurmaz.
|
||||||
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) göre filtreler; havuz
|
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) ve varsa danışman
|
||||||
* 24'ün altına düşerse kademeli gevşetir (önce il, sonra kategori) ki liste
|
* profiline (dil, öğretim türü, burs, ücret, km) göre filtreler; havuz 24'ün
|
||||||
* her zaman kurulabilsin.
|
* altına düşerse kademeli gevşetir (önce profil, sonra il, sonra kategori) ki
|
||||||
|
* liste her zaman kurulabilsin. /meraklisina şeması bu sırayı anlatır.
|
||||||
*/
|
*/
|
||||||
export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
export function havuzOlustur(
|
||||||
|
params: RaporParams,
|
||||||
|
profil?: DanismanProfili,
|
||||||
|
): HavuzSonuc {
|
||||||
const uniturGrubu =
|
const uniturGrubu =
|
||||||
params.universiteTipi && params.universiteTipi !== "farketmez"
|
params.universiteTipi && params.universiteTipi !== "farketmez"
|
||||||
? params.universiteTipi
|
? params.universiteTipi
|
||||||
@@ -110,6 +161,30 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
|||||||
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
|
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
|
||||||
: havuz;
|
: havuz;
|
||||||
|
|
||||||
|
// 0. Profil süzgeci (danışmana söylenenler) — yalnız yeterli havuz bırakırsa.
|
||||||
|
// Bırakmazsa profil gevşetilir ve aşağıdaki sihirbaz akışı aynen sürer;
|
||||||
|
// liste yine kurulur, gevşetme yapay zekâya ve adaya söylenir.
|
||||||
|
const suzgec = profilSuzgeci(profil, iller.length > 0);
|
||||||
|
const profilGevsetildi: Gevsetilen[] = [];
|
||||||
|
if (suzgec.ozellik || suzgec.iller) {
|
||||||
|
const profilliHam = dilimEkle(
|
||||||
|
searchByRank(params.sira, params.tur, {
|
||||||
|
iller: iller.length > 0 ? iller : suzgec.iller,
|
||||||
|
uniturGrubu,
|
||||||
|
ozellik: suzgec.ozellik,
|
||||||
|
limitPerBucket: LIMIT,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const profilliFiltreli = kategoriFiltre(profilliHam);
|
||||||
|
if (profilliFiltreli.length >= HEDEF) {
|
||||||
|
return {
|
||||||
|
havuz: dilimBasinaKirp(profilliFiltreli, PROMPT_LIMIT_DILIM),
|
||||||
|
gevsetildi: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
profilGevsetildi.push("profil");
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
|
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
|
||||||
const tamHam = dilimEkle(
|
const tamHam = dilimEkle(
|
||||||
searchByRank(params.sira, params.tur, {
|
searchByRank(params.sira, params.tur, {
|
||||||
@@ -122,7 +197,7 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
|||||||
if (tamFiltreli.length >= HEDEF) {
|
if (tamFiltreli.length >= HEDEF) {
|
||||||
return {
|
return {
|
||||||
havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM),
|
havuz: dilimBasinaKirp(tamFiltreli, PROMPT_LIMIT_DILIM),
|
||||||
gevsetildi: [],
|
gevsetildi: profilGevsetildi,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,12 +205,12 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
|||||||
if (kategoriler.length > 0 && tamHam.length >= HEDEF) {
|
if (kategoriler.length > 0 && tamHam.length >= HEDEF) {
|
||||||
return {
|
return {
|
||||||
havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
|
havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
|
||||||
gevsetildi: ["kategori"],
|
gevsetildi: [...profilGevsetildi, "kategori"],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
|
// 3. İl gevşet: il filtresini kaldırıp kategoriye geri dön
|
||||||
const gevsetildi: ("kategori" | "il")[] = [];
|
const gevsetildi: Gevsetilen[] = [...profilGevsetildi];
|
||||||
if (iller.length > 0) gevsetildi.push("il");
|
if (iller.length > 0) gevsetildi.push("il");
|
||||||
const ilsizHam = dilimEkle(
|
const ilsizHam = dilimEkle(
|
||||||
searchByRank(params.sira, params.tur, {
|
searchByRank(params.sira, params.tur, {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
//
|
//
|
||||||
// Öğrenci = örgün (birinci + ikinci öğretim), tüm düzeyler; uzaktan ve açık
|
// Öğrenci = örgün (birinci + ikinci öğretim), tüm düzeyler; uzaktan ve açık
|
||||||
// öğretim dahil değil. Öğretim üyesi = Prof. + Doç. + Dr. Öğr. Üyesi.
|
// öğretim dahil değil. Öğretim üyesi = Prof. + Doç. + Dr. Öğr. Üyesi.
|
||||||
|
// Kadın/erkek = örgün ön lisans + lisans öğrencisi (E/K sütunları).
|
||||||
// Anahtar: /universite/[slug] sayfa slug'ı. Burada olmayan üniversitenin
|
// Anahtar: /universite/[slug] sayfa slug'ı. Burada olmayan üniversitenin
|
||||||
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
||||||
|
|
||||||
@@ -22,213 +23,217 @@ export type UniIstatistik = {
|
|||||||
ogretimUyesi: number;
|
ogretimUyesi: number;
|
||||||
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
||||||
akademisyen: number;
|
akademisyen: number;
|
||||||
|
/** Örgün ön lisans + lisans kadın öğrenci */
|
||||||
|
kadin: number;
|
||||||
|
/** Örgün ön lisans + lisans erkek öğrenci */
|
||||||
|
erkek: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const YOK_ISTATISTIK: Record<string, UniIstatistik> = {
|
export const YOK_ISTATISTIK: Record<string, UniIstatistik> = {
|
||||||
"abdullah-gul-universitesi": { ogrenci: 3936, ogretimUyesi: 107, akademisyen: 300 },
|
"abdullah-gul-universitesi": { ogrenci: 3936, ogretimUyesi: 107, akademisyen: 300, kadin: 1931, erkek: 1761 },
|
||||||
"acibadem-mehmet-ali-aydinlar-universitesi": { ogrenci: 6279, ogretimUyesi: 375, akademisyen: 646 },
|
"acibadem-mehmet-ali-aydinlar-universitesi": { ogrenci: 6279, ogretimUyesi: 375, akademisyen: 646, kadin: 4250, erkek: 1578 },
|
||||||
"adana-alparslan-turkes-bilim-ve-teknoloji-universitesi": { ogrenci: 6220, ogretimUyesi: 236, akademisyen: 446 },
|
"adana-alparslan-turkes-bilim-ve-teknoloji-universitesi": { ogrenci: 6220, ogretimUyesi: 236, akademisyen: 446, kadin: 2405, erkek: 3131 },
|
||||||
"adiyaman-universitesi": { ogrenci: 16659, ogretimUyesi: 548, akademisyen: 862 },
|
"adiyaman-universitesi": { ogrenci: 16659, ogretimUyesi: 548, akademisyen: 862, kadin: 7932, erkek: 8035 },
|
||||||
"afyon-kocatepe-universitesi": { ogrenci: 29711, ogretimUyesi: 699, akademisyen: 1109 },
|
"afyon-kocatepe-universitesi": { ogrenci: 29711, ogretimUyesi: 699, akademisyen: 1109, kadin: 13555, erkek: 13599 },
|
||||||
"afyonkarahisar-saglik-bilimleri-universitesi": { ogrenci: 7072, ogretimUyesi: 325, akademisyen: 620 },
|
"afyonkarahisar-saglik-bilimleri-universitesi": { ogrenci: 7072, ogretimUyesi: 325, akademisyen: 620, kadin: 4759, erkek: 2112 },
|
||||||
"agri-ibrahim-cecen-universitesi": { ogrenci: 15239, ogretimUyesi: 390, akademisyen: 574 },
|
"agri-ibrahim-cecen-universitesi": { ogrenci: 15239, ogretimUyesi: 390, akademisyen: 574, kadin: 8002, erkek: 6099 },
|
||||||
"akdeniz-universitesi": { ogrenci: 55992, ogretimUyesi: 1339, akademisyen: 2658 },
|
"akdeniz-universitesi": { ogrenci: 55992, ogretimUyesi: 1339, akademisyen: 2658, kadin: 24232, erkek: 27483 },
|
||||||
"aksaray-universitesi": { ogrenci: 18151, ogretimUyesi: 626, akademisyen: 963 },
|
"aksaray-universitesi": { ogrenci: 18151, ogretimUyesi: 626, akademisyen: 963, kadin: 8731, erkek: 7227 },
|
||||||
"alanya-alaaddin-keykubat-universitesi": { ogrenci: 15322, ogretimUyesi: 410, akademisyen: 696 },
|
"alanya-alaaddin-keykubat-universitesi": { ogrenci: 15322, ogretimUyesi: 410, akademisyen: 696, kadin: 7155, erkek: 7297 },
|
||||||
"alanya-universitesi": { ogrenci: 1207, ogretimUyesi: 51, akademisyen: 100 },
|
"alanya-universitesi": { ogrenci: 1207, ogretimUyesi: 51, akademisyen: 100, kadin: 606, erkek: 553 },
|
||||||
"altinbas-universitesi": { ogrenci: 12055, ogretimUyesi: 227, akademisyen: 334 },
|
"altinbas-universitesi": { ogrenci: 12055, ogretimUyesi: 227, akademisyen: 334, kadin: 5464, erkek: 5583 },
|
||||||
"amasya-universitesi": { ogrenci: 16656, ogretimUyesi: 543, akademisyen: 748 },
|
"amasya-universitesi": { ogrenci: 16656, ogretimUyesi: 543, akademisyen: 748, kadin: 8278, erkek: 6878 },
|
||||||
"anadolu-universitesi": { ogrenci: 25934, ogretimUyesi: 900, akademisyen: 1470 },
|
"anadolu-universitesi": { ogrenci: 25934, ogretimUyesi: 900, akademisyen: 1470, kadin: 12311, erkek: 9371 },
|
||||||
"ankara-bilim-universitesi": { ogrenci: 3626, ogretimUyesi: 72, akademisyen: 134 },
|
"ankara-bilim-universitesi": { ogrenci: 3626, ogretimUyesi: 72, akademisyen: 134, kadin: 1813, erkek: 1689 },
|
||||||
"ankara-haci-bayram-veli-universitesi": { ogrenci: 27559, ogretimUyesi: 867, akademisyen: 1221 },
|
"ankara-haci-bayram-veli-universitesi": { ogrenci: 27559, ogretimUyesi: 867, akademisyen: 1221, kadin: 13583, erkek: 9130 },
|
||||||
"ankara-medipol-universitesi": { ogrenci: 14035, ogretimUyesi: 270, akademisyen: 458 },
|
"ankara-medipol-universitesi": { ogrenci: 14035, ogretimUyesi: 270, akademisyen: 458, kadin: 9131, erkek: 4696 },
|
||||||
"ankara-muzik-ve-guzel-sanatlar-universitesi": { ogrenci: 1457, ogretimUyesi: 86, akademisyen: 136 },
|
"ankara-muzik-ve-guzel-sanatlar-universitesi": { ogrenci: 1457, ogretimUyesi: 86, akademisyen: 136, kadin: 588, erkek: 613 },
|
||||||
"ankara-sosyal-bilimler-universitesi": { ogrenci: 5879, ogretimUyesi: 274, akademisyen: 477 },
|
"ankara-sosyal-bilimler-universitesi": { ogrenci: 5879, ogretimUyesi: 274, akademisyen: 477, kadin: 2747, erkek: 1734 },
|
||||||
"ankara-universitesi": { ogrenci: 56128, ogretimUyesi: 2225, akademisyen: 3702 },
|
"ankara-universitesi": { ogrenci: 56128, ogretimUyesi: 2225, akademisyen: 3702, kadin: 26197, erkek: 21099 },
|
||||||
"ankara-yildirim-beyazit-universitesi": { ogrenci: 26177, ogretimUyesi: 1019, akademisyen: 1568 },
|
"ankara-yildirim-beyazit-universitesi": { ogrenci: 26177, ogretimUyesi: 1019, akademisyen: 1568, kadin: 13367, erkek: 8801 },
|
||||||
"antalya-belek-universitesi": { ogrenci: 2553, ogretimUyesi: 44, akademisyen: 94 },
|
"antalya-belek-universitesi": { ogrenci: 2553, ogretimUyesi: 44, akademisyen: 94, kadin: 1220, erkek: 1316 },
|
||||||
"antalya-bilim-universitesi": { ogrenci: 8597, ogretimUyesi: 180, akademisyen: 315 },
|
"antalya-bilim-universitesi": { ogrenci: 8597, ogretimUyesi: 180, akademisyen: 315, kadin: 4696, erkek: 3440 },
|
||||||
"ardahan-universitesi": { ogrenci: 7355, ogretimUyesi: 236, akademisyen: 380 },
|
"ardahan-universitesi": { ogrenci: 7355, ogretimUyesi: 236, akademisyen: 380, kadin: 3751, erkek: 3092 },
|
||||||
"artvin-coruh-universitesi": { ogrenci: 12358, ogretimUyesi: 332, akademisyen: 566 },
|
"artvin-coruh-universitesi": { ogrenci: 12358, ogretimUyesi: 332, akademisyen: 566, kadin: 6945, erkek: 4732 },
|
||||||
"atasehir-adiguzel-meslek-yuksekokulu": { ogrenci: 1476, ogretimUyesi: 2, akademisyen: 32 },
|
"atasehir-adiguzel-meslek-yuksekokulu": { ogrenci: 1476, ogretimUyesi: 2, akademisyen: 32, kadin: 501, erkek: 975 },
|
||||||
"ataturk-universitesi": { ogrenci: 58279, ogretimUyesi: 1800, akademisyen: 2758 },
|
"ataturk-universitesi": { ogrenci: 58279, ogretimUyesi: 1800, akademisyen: 2758, kadin: 25889, erkek: 23030 },
|
||||||
"atilim-universitesi": { ogrenci: 9885, ogretimUyesi: 331, akademisyen: 594 },
|
"atilim-universitesi": { ogrenci: 9885, ogretimUyesi: 331, akademisyen: 594, kadin: 4548, erkek: 4688 },
|
||||||
"avrasya-universitesi": { ogrenci: 7495, ogretimUyesi: 119, akademisyen: 238 },
|
"avrasya-universitesi": { ogrenci: 7495, ogretimUyesi: 119, akademisyen: 238, kadin: 4251, erkek: 3049 },
|
||||||
"aydin-adnan-menderes-universitesi": { ogrenci: 43686, ogretimUyesi: 1232, akademisyen: 1924 },
|
"aydin-adnan-menderes-universitesi": { ogrenci: 43686, ogretimUyesi: 1232, akademisyen: 1924, kadin: 22196, erkek: 18397 },
|
||||||
"bahcesehir-universitesi": { ogrenci: 21181, ogretimUyesi: 560, akademisyen: 838 },
|
"bahcesehir-universitesi": { ogrenci: 21181, ogretimUyesi: 560, akademisyen: 838, kadin: 9131, erkek: 9225 },
|
||||||
"balikesir-universitesi": { ogrenci: 28673, ogretimUyesi: 890, akademisyen: 1313 },
|
"balikesir-universitesi": { ogrenci: 28673, ogretimUyesi: 890, akademisyen: 1313, kadin: 13598, erkek: 13151 },
|
||||||
"bandirma-onyedi-eylul-universitesi": { ogrenci: 18949, ogretimUyesi: 456, akademisyen: 653 },
|
"bandirma-onyedi-eylul-universitesi": { ogrenci: 18949, ogretimUyesi: 456, akademisyen: 653, kadin: 9102, erkek: 8604 },
|
||||||
"bartin-universitesi": { ogrenci: 15615, ogretimUyesi: 452, akademisyen: 723 },
|
"bartin-universitesi": { ogrenci: 15615, ogretimUyesi: 452, akademisyen: 723, kadin: 7726, erkek: 6440 },
|
||||||
"baskent-universitesi": { ogrenci: 17784, ogretimUyesi: 976, akademisyen: 1920 },
|
"baskent-universitesi": { ogrenci: 17784, ogretimUyesi: 976, akademisyen: 1920, kadin: 9278, erkek: 7051 },
|
||||||
"batman-universitesi": { ogrenci: 14701, ogretimUyesi: 360, akademisyen: 584 },
|
"batman-universitesi": { ogrenci: 14701, ogretimUyesi: 360, akademisyen: 584, kadin: 5596, erkek: 7057 },
|
||||||
"bayburt-universitesi": { ogrenci: 13883, ogretimUyesi: 314, akademisyen: 499 },
|
"bayburt-universitesi": { ogrenci: 13883, ogretimUyesi: 314, akademisyen: 499, kadin: 6505, erkek: 6064 },
|
||||||
"beykoz-universitesi": { ogrenci: 5251, ogretimUyesi: 94, akademisyen: 154 },
|
"beykoz-universitesi": { ogrenci: 5251, ogretimUyesi: 94, akademisyen: 154, kadin: 1729, erkek: 2385 },
|
||||||
"bezm-i-alem-vakif-universitesi": { ogrenci: 4076, ogretimUyesi: 231, akademisyen: 605 },
|
"bezm-i-alem-vakif-universitesi": { ogrenci: 4076, ogretimUyesi: 231, akademisyen: 605, kadin: 2861, erkek: 984 },
|
||||||
"bilecik-seyh-edebali-universitesi": { ogrenci: 19612, ogretimUyesi: 495, akademisyen: 824 },
|
"bilecik-seyh-edebali-universitesi": { ogrenci: 19612, ogretimUyesi: 495, akademisyen: 824, kadin: 9538, erkek: 8931 },
|
||||||
"bingol-universitesi": { ogrenci: 18558, ogretimUyesi: 494, akademisyen: 744 },
|
"bingol-universitesi": { ogrenci: 18558, ogretimUyesi: 494, akademisyen: 744, kadin: 8860, erkek: 8174 },
|
||||||
"biruni-universitesi": { ogrenci: 15154, ogretimUyesi: 347, akademisyen: 449 },
|
"biruni-universitesi": { ogrenci: 15154, ogretimUyesi: 347, akademisyen: 449, kadin: 9944, erkek: 4464 },
|
||||||
"bitlis-eren-universitesi": { ogrenci: 14635, ogretimUyesi: 342, akademisyen: 549 },
|
"bitlis-eren-universitesi": { ogrenci: 14635, ogretimUyesi: 342, akademisyen: 549, kadin: 6782, erkek: 6699 },
|
||||||
"bogazici-universitesi": { ogrenci: 13839, ogretimUyesi: 503, akademisyen: 1031 },
|
"bogazici-universitesi": { ogrenci: 13839, ogretimUyesi: 503, akademisyen: 1031, kadin: 5200, erkek: 6645 },
|
||||||
"bolu-abant-izzet-baysal-universitesi": { ogrenci: 30443, ogretimUyesi: 965, akademisyen: 1491 },
|
"bolu-abant-izzet-baysal-universitesi": { ogrenci: 30443, ogretimUyesi: 965, akademisyen: 1491, kadin: 15678, erkek: 12592 },
|
||||||
"burdur-mehmet-akif-ersoy-universitesi": { ogrenci: 34361, ogretimUyesi: 799, akademisyen: 1209 },
|
"burdur-mehmet-akif-ersoy-universitesi": { ogrenci: 34361, ogretimUyesi: 799, akademisyen: 1209, kadin: 16767, erkek: 15004 },
|
||||||
"bursa-teknik-universitesi": { ogrenci: 12346, ogretimUyesi: 350, akademisyen: 601 },
|
"bursa-teknik-universitesi": { ogrenci: 12346, ogretimUyesi: 350, akademisyen: 601, kadin: 4583, erkek: 4884 },
|
||||||
"bursa-uludag-universitesi": { ogrenci: 58341, ogretimUyesi: 1368, akademisyen: 2331 },
|
"bursa-uludag-universitesi": { ogrenci: 58341, ogretimUyesi: 1368, akademisyen: 2331, kadin: 25706, erkek: 26522 },
|
||||||
"cag-universitesi": { ogrenci: 3667, ogretimUyesi: 63, akademisyen: 127 },
|
"cag-universitesi": { ogrenci: 3667, ogretimUyesi: 63, akademisyen: 127, kadin: 1989, erkek: 1435 },
|
||||||
"canakkale-onsekiz-mart-universitesi": { ogrenci: 42341, ogretimUyesi: 1254, akademisyen: 2058 },
|
"canakkale-onsekiz-mart-universitesi": { ogrenci: 42341, ogretimUyesi: 1254, akademisyen: 2058, kadin: 21004, erkek: 17456 },
|
||||||
"cankaya-universitesi": { ogrenci: 6401, ogretimUyesi: 148, akademisyen: 260 },
|
"cankaya-universitesi": { ogrenci: 6401, ogretimUyesi: 148, akademisyen: 260, kadin: 2651, erkek: 3040 },
|
||||||
"cankiri-karatekin-universitesi": { ogrenci: 17291, ogretimUyesi: 488, akademisyen: 740 },
|
"cankiri-karatekin-universitesi": { ogrenci: 17291, ogretimUyesi: 488, akademisyen: 740, kadin: 8324, erkek: 7207 },
|
||||||
"cukurova-universitesi": { ogrenci: 44430, ogretimUyesi: 1187, akademisyen: 2116 },
|
"cukurova-universitesi": { ogrenci: 44430, ogretimUyesi: 1187, akademisyen: 2116, kadin: 18383, erkek: 21623 },
|
||||||
"demiroglu-bilim-universitesi": { ogrenci: 2633, ogretimUyesi: 179, akademisyen: 231 },
|
"demiroglu-bilim-universitesi": { ogrenci: 2633, ogretimUyesi: 179, akademisyen: 231, kadin: 1861, erkek: 693 },
|
||||||
"dicle-universitesi": { ogrenci: 42368, ogretimUyesi: 1011, akademisyen: 1641 },
|
"dicle-universitesi": { ogrenci: 42368, ogretimUyesi: 1011, akademisyen: 1641, kadin: 18148, erkek: 21068 },
|
||||||
"dogus-universitesi": { ogrenci: 18442, ogretimUyesi: 217, akademisyen: 459 },
|
"dogus-universitesi": { ogrenci: 18442, ogretimUyesi: 217, akademisyen: 459, kadin: 8055, erkek: 9643 },
|
||||||
"dokuz-eylul-universitesi": { ogrenci: 56833, ogretimUyesi: 1849, akademisyen: 2951 },
|
"dokuz-eylul-universitesi": { ogrenci: 56833, ogretimUyesi: 1849, akademisyen: 2951, kadin: 23970, erkek: 27166 },
|
||||||
"duzce-universitesi": { ogrenci: 28561, ogretimUyesi: 765, akademisyen: 1230 },
|
"duzce-universitesi": { ogrenci: 28561, ogretimUyesi: 765, akademisyen: 1230, kadin: 12819, erkek: 13616 },
|
||||||
"ege-universitesi": { ogrenci: 49852, ogretimUyesi: 1744, akademisyen: 2983 },
|
"ege-universitesi": { ogrenci: 49852, ogretimUyesi: 1744, akademisyen: 2983, kadin: 21438, erkek: 22507 },
|
||||||
"erciyes-universitesi": { ogrenci: 42735, ogretimUyesi: 1237, akademisyen: 2195 },
|
"erciyes-universitesi": { ogrenci: 42735, ogretimUyesi: 1237, akademisyen: 2195, kadin: 18111, erkek: 17968 },
|
||||||
"erzincan-binali-yildirim-universitesi": { ogrenci: 23617, ogretimUyesi: 764, akademisyen: 1070 },
|
"erzincan-binali-yildirim-universitesi": { ogrenci: 23617, ogretimUyesi: 764, akademisyen: 1070, kadin: 11460, erkek: 10260 },
|
||||||
"erzurum-teknik-universitesi": { ogrenci: 5876, ogretimUyesi: 268, akademisyen: 389 },
|
"erzurum-teknik-universitesi": { ogrenci: 5876, ogretimUyesi: 268, akademisyen: 389, kadin: 2530, erkek: 2245 },
|
||||||
"eskisehir-osmangazi-universitesi": { ogrenci: 26667, ogretimUyesi: 1043, akademisyen: 1695 },
|
"eskisehir-osmangazi-universitesi": { ogrenci: 26667, ogretimUyesi: 1043, akademisyen: 1695, kadin: 11573, erkek: 12061 },
|
||||||
"eskisehir-teknik-universitesi": { ogrenci: 13840, ogretimUyesi: 467, akademisyen: 735 },
|
"eskisehir-teknik-universitesi": { ogrenci: 13840, ogretimUyesi: 467, akademisyen: 735, kadin: 4874, erkek: 7422 },
|
||||||
"fatih-sultan-mehmet-vakif-universitesi": { ogrenci: 7676, ogretimUyesi: 169, akademisyen: 328 },
|
"fatih-sultan-mehmet-vakif-universitesi": { ogrenci: 7676, ogretimUyesi: 169, akademisyen: 328, kadin: 3639, erkek: 2774 },
|
||||||
"fenerbahce-universitesi": { ogrenci: 9741, ogretimUyesi: 195, akademisyen: 311 },
|
"fenerbahce-universitesi": { ogrenci: 9741, ogretimUyesi: 195, akademisyen: 311, kadin: 5072, erkek: 3918 },
|
||||||
"firat-universitesi": { ogrenci: 39181, ogretimUyesi: 1167, akademisyen: 1924 },
|
"firat-universitesi": { ogrenci: 39181, ogretimUyesi: 1167, akademisyen: 1924, kadin: 16028, erkek: 18301 },
|
||||||
"galatasaray-universitesi": { ogrenci: 4398, ogretimUyesi: 162, akademisyen: 310 },
|
"galatasaray-universitesi": { ogrenci: 4398, ogretimUyesi: 162, akademisyen: 310, kadin: 1655, erkek: 1521 },
|
||||||
"gazi-universitesi": { ogrenci: 38023, ogretimUyesi: 1877, akademisyen: 3115 },
|
"gazi-universitesi": { ogrenci: 38023, ogretimUyesi: 1877, akademisyen: 3115, kadin: 15208, erkek: 14515 },
|
||||||
"gaziantep-islam-bilim-ve-teknoloji-universitesi": { ogrenci: 5934, ogretimUyesi: 167, akademisyen: 248 },
|
"gaziantep-islam-bilim-ve-teknoloji-universitesi": { ogrenci: 5934, ogretimUyesi: 167, akademisyen: 248, kadin: 3341, erkek: 2249 },
|
||||||
"gaziantep-universitesi": { ogrenci: 45986, ogretimUyesi: 906, akademisyen: 1689 },
|
"gaziantep-universitesi": { ogrenci: 45986, ogretimUyesi: 906, akademisyen: 1689, kadin: 20656, erkek: 20524 },
|
||||||
"gebze-teknik-universitesi": { ogrenci: 10152, ogretimUyesi: 354, akademisyen: 820 },
|
"gebze-teknik-universitesi": { ogrenci: 10152, ogretimUyesi: 354, akademisyen: 820, kadin: 3183, erkek: 4217 },
|
||||||
"giresun-universitesi": { ogrenci: 24556, ogretimUyesi: 820, akademisyen: 1165 },
|
"giresun-universitesi": { ogrenci: 24556, ogretimUyesi: 820, akademisyen: 1165, kadin: 12018, erkek: 10984 },
|
||||||
"gumushane-universitesi": { ogrenci: 19558, ogretimUyesi: 425, akademisyen: 664 },
|
"gumushane-universitesi": { ogrenci: 19558, ogretimUyesi: 425, akademisyen: 664, kadin: 9622, erkek: 8329 },
|
||||||
"hacettepe-universitesi": { ogrenci: 45592, ogretimUyesi: 2015, akademisyen: 3536 },
|
"hacettepe-universitesi": { ogrenci: 45592, ogretimUyesi: 2015, akademisyen: 3536, kadin: 20858, erkek: 17305 },
|
||||||
"hakkari-universitesi": { ogrenci: 5255, ogretimUyesi: 216, akademisyen: 332 },
|
"hakkari-universitesi": { ogrenci: 5255, ogretimUyesi: 216, akademisyen: 332, kadin: 2459, erkek: 2485 },
|
||||||
"halic-universitesi": { ogrenci: 15757, ogretimUyesi: 256, akademisyen: 470 },
|
"halic-universitesi": { ogrenci: 15757, ogretimUyesi: 256, akademisyen: 470, kadin: 8201, erkek: 6671 },
|
||||||
"harran-universitesi": { ogrenci: 28476, ogretimUyesi: 740, akademisyen: 1249 },
|
"harran-universitesi": { ogrenci: 28476, ogretimUyesi: 740, akademisyen: 1249, kadin: 13266, erkek: 13153 },
|
||||||
"hasan-kalyoncu-universitesi": { ogrenci: 7697, ogretimUyesi: 206, akademisyen: 311 },
|
"hasan-kalyoncu-universitesi": { ogrenci: 7697, ogretimUyesi: 206, akademisyen: 311, kadin: 4304, erkek: 2595 },
|
||||||
"hatay-mustafa-kemal-universitesi": { ogrenci: 18471, ogretimUyesi: 685, akademisyen: 1146 },
|
"hatay-mustafa-kemal-universitesi": { ogrenci: 18471, ogretimUyesi: 685, akademisyen: 1146, kadin: 8223, erkek: 8149 },
|
||||||
"hitit-universitesi": { ogrenci: 18433, ogretimUyesi: 492, akademisyen: 821 },
|
"hitit-universitesi": { ogrenci: 18433, ogretimUyesi: 492, akademisyen: 821, kadin: 8416, erkek: 8700 },
|
||||||
"ibn-haldun-universitesi": { ogrenci: 1877, ogretimUyesi: 110, akademisyen: 171 },
|
"ibn-haldun-universitesi": { ogrenci: 1877, ogretimUyesi: 110, akademisyen: 171, kadin: 437, erkek: 337 },
|
||||||
"igdir-universitesi": { ogrenci: 13947, ogretimUyesi: 314, akademisyen: 491 },
|
"igdir-universitesi": { ogrenci: 13947, ogretimUyesi: 314, akademisyen: 491, kadin: 6633, erkek: 5886 },
|
||||||
"ihsan-dogramaci-bilkent-universitesi": { ogrenci: 12244, ogretimUyesi: 367, akademisyen: 707 },
|
"ihsan-dogramaci-bilkent-universitesi": { ogrenci: 12244, ogretimUyesi: 367, akademisyen: 707, kadin: 5445, erkek: 5633 },
|
||||||
"inonu-universitesi": { ogrenci: 30854, ogretimUyesi: 948, akademisyen: 1584 },
|
"inonu-universitesi": { ogrenci: 30854, ogretimUyesi: 948, akademisyen: 1584, kadin: 14947, erkek: 12717 },
|
||||||
"isik-universitesi": { ogrenci: 5999, ogretimUyesi: 108, akademisyen: 189 },
|
"isik-universitesi": { ogrenci: 5999, ogretimUyesi: 108, akademisyen: 189, kadin: 2740, erkek: 2994 },
|
||||||
"iskenderun-teknik-universitesi": { ogrenci: 13168, ogretimUyesi: 263, akademisyen: 388 },
|
"iskenderun-teknik-universitesi": { ogrenci: 13168, ogretimUyesi: 263, akademisyen: 388, kadin: 4192, erkek: 8139 },
|
||||||
"isparta-uygulamali-bilimler-universitesi": { ogrenci: 22373, ogretimUyesi: 415, akademisyen: 739 },
|
"isparta-uygulamali-bilimler-universitesi": { ogrenci: 22373, ogretimUyesi: 415, akademisyen: 739, kadin: 9500, erkek: 11945 },
|
||||||
"istanbul-29-mayis-universitesi": { ogrenci: 5492, ogretimUyesi: 129, akademisyen: 187 },
|
"istanbul-29-mayis-universitesi": { ogrenci: 5492, ogretimUyesi: 129, akademisyen: 187, kadin: 3330, erkek: 1698 },
|
||||||
"istanbul-arel-universitesi": { ogrenci: 14076, ogretimUyesi: 276, akademisyen: 519 },
|
"istanbul-arel-universitesi": { ogrenci: 14076, ogretimUyesi: 276, akademisyen: 519, kadin: 5603, erkek: 6012 },
|
||||||
"istanbul-atlas-universitesi": { ogrenci: 10704, ogretimUyesi: 376, akademisyen: 501 },
|
"istanbul-atlas-universitesi": { ogrenci: 10704, ogretimUyesi: 376, akademisyen: 501, kadin: 6128, erkek: 3872 },
|
||||||
"istanbul-aydin-universitesi": { ogrenci: 47200, ogretimUyesi: 777, akademisyen: 1200 },
|
"istanbul-aydin-universitesi": { ogrenci: 47200, ogretimUyesi: 777, akademisyen: 1200, kadin: 22065, erkek: 22829 },
|
||||||
"istanbul-beykent-universitesi": { ogrenci: 26693, ogretimUyesi: 501, akademisyen: 824 },
|
"istanbul-beykent-universitesi": { ogrenci: 26693, ogretimUyesi: 501, akademisyen: 824, kadin: 12165, erkek: 12578 },
|
||||||
"istanbul-bilgi-universitesi": { ogrenci: 19216, ogretimUyesi: 310, akademisyen: 568 },
|
"istanbul-bilgi-universitesi": { ogrenci: 19216, ogretimUyesi: 310, akademisyen: 568, kadin: 9301, erkek: 8697 },
|
||||||
"istanbul-esenyurt-universitesi": { ogrenci: 14673, ogretimUyesi: 150, akademisyen: 320 },
|
"istanbul-esenyurt-universitesi": { ogrenci: 14673, ogretimUyesi: 150, akademisyen: 320, kadin: 5961, erkek: 6142 },
|
||||||
"istanbul-galata-universitesi": { ogrenci: 4213, ogretimUyesi: 125, akademisyen: 192 },
|
"istanbul-galata-universitesi": { ogrenci: 4213, ogretimUyesi: 125, akademisyen: 192, kadin: 2318, erkek: 1527 },
|
||||||
"istanbul-gedik-universitesi": { ogrenci: 10243, ogretimUyesi: 175, akademisyen: 306 },
|
"istanbul-gedik-universitesi": { ogrenci: 10243, ogretimUyesi: 175, akademisyen: 306, kadin: 3828, erkek: 4777 },
|
||||||
"istanbul-gelisim-universitesi": { ogrenci: 36730, ogretimUyesi: 472, akademisyen: 775 },
|
"istanbul-gelisim-universitesi": { ogrenci: 36730, ogretimUyesi: 472, akademisyen: 775, kadin: 15462, erkek: 19072 },
|
||||||
"istanbul-kent-universitesi": { ogrenci: 15262, ogretimUyesi: 231, akademisyen: 405 },
|
"istanbul-kent-universitesi": { ogrenci: 15262, ogretimUyesi: 231, akademisyen: 405, kadin: 8327, erkek: 5516 },
|
||||||
"istanbul-kultur-universitesi": { ogrenci: 13322, ogretimUyesi: 268, akademisyen: 459 },
|
"istanbul-kultur-universitesi": { ogrenci: 13322, ogretimUyesi: 268, akademisyen: 459, kadin: 5923, erkek: 6236 },
|
||||||
"istanbul-medeniyet-universitesi": { ogrenci: 15978, ogretimUyesi: 746, akademisyen: 1141 },
|
"istanbul-medeniyet-universitesi": { ogrenci: 15978, ogretimUyesi: 746, akademisyen: 1141, kadin: 8362, erkek: 6266 },
|
||||||
"istanbul-medipol-universitesi": { ogrenci: 49066, ogretimUyesi: 975, akademisyen: 1647 },
|
"istanbul-medipol-universitesi": { ogrenci: 49066, ogretimUyesi: 975, akademisyen: 1647, kadin: 29514, erkek: 17933 },
|
||||||
"istanbul-nisantasi-universitesi": { ogrenci: 42789, ogretimUyesi: 485, akademisyen: 783 },
|
"istanbul-nisantasi-universitesi": { ogrenci: 42789, ogretimUyesi: 485, akademisyen: 783, kadin: 16369, erkek: 24229 },
|
||||||
"istanbul-okan-universitesi": { ogrenci: 17022, ogretimUyesi: 560, akademisyen: 823 },
|
"istanbul-okan-universitesi": { ogrenci: 17022, ogretimUyesi: 560, akademisyen: 823, kadin: 7519, erkek: 7264 },
|
||||||
"istanbul-rumeli-universitesi": { ogrenci: 10118, ogretimUyesi: 165, akademisyen: 257 },
|
"istanbul-rumeli-universitesi": { ogrenci: 10118, ogretimUyesi: 165, akademisyen: 257, kadin: 4165, erkek: 5081 },
|
||||||
"istanbul-sabahattin-zaim-universitesi": { ogrenci: 8740, ogretimUyesi: 261, akademisyen: 392 },
|
"istanbul-sabahattin-zaim-universitesi": { ogrenci: 8740, ogretimUyesi: 261, akademisyen: 392, kadin: 4988, erkek: 2764 },
|
||||||
"istanbul-saglik-ve-teknoloji-universitesi": { ogrenci: 5136, ogretimUyesi: 350, akademisyen: 434 },
|
"istanbul-saglik-ve-teknoloji-universitesi": { ogrenci: 5136, ogretimUyesi: 350, akademisyen: 434, kadin: 2993, erkek: 2052 },
|
||||||
"istanbul-sisli-meslek-yuksekokulu": { ogrenci: 10446, ogretimUyesi: 19, akademisyen: 80 },
|
"istanbul-sisli-meslek-yuksekokulu": { ogrenci: 10446, ogretimUyesi: 19, akademisyen: 80, kadin: 5364, erkek: 5082 },
|
||||||
"istanbul-teknik-universitesi": { ogrenci: 33900, ogretimUyesi: 1253, akademisyen: 2524 },
|
"istanbul-teknik-universitesi": { ogrenci: 33900, ogretimUyesi: 1253, akademisyen: 2524, kadin: 7933, erkek: 15180 },
|
||||||
"istanbul-ticaret-universitesi": { ogrenci: 8384, ogretimUyesi: 167, akademisyen: 270 },
|
"istanbul-ticaret-universitesi": { ogrenci: 8384, ogretimUyesi: 167, akademisyen: 270, kadin: 3338, erkek: 3583 },
|
||||||
"istanbul-topkapi-universitesi": { ogrenci: 24477, ogretimUyesi: 237, akademisyen: 424 },
|
"istanbul-topkapi-universitesi": { ogrenci: 24477, ogretimUyesi: 237, akademisyen: 424, kadin: 10362, erkek: 11967 },
|
||||||
"istanbul-universitesi": { ogrenci: 52770, ogretimUyesi: 2125, akademisyen: 3379 },
|
"istanbul-universitesi": { ogrenci: 52770, ogretimUyesi: 2125, akademisyen: 3379, kadin: 22750, erkek: 19201 },
|
||||||
"istanbul-universitesi-cerrahpasa": { ogrenci: 29490, ogretimUyesi: 1318, akademisyen: 2053 },
|
"istanbul-universitesi-cerrahpasa": { ogrenci: 29490, ogretimUyesi: 1318, akademisyen: 2053, kadin: 12244, erkek: 11973 },
|
||||||
"istanbul-yeni-yuzyil-universitesi": { ogrenci: 20511, ogretimUyesi: 271, akademisyen: 424 },
|
"istanbul-yeni-yuzyil-universitesi": { ogrenci: 20511, ogretimUyesi: 271, akademisyen: 424, kadin: 10571, erkek: 9173 },
|
||||||
"istinye-universitesi": { ogrenci: 17697, ogretimUyesi: 525, akademisyen: 745 },
|
"istinye-universitesi": { ogrenci: 17697, ogretimUyesi: 525, akademisyen: 745, kadin: 9249, erkek: 6682 },
|
||||||
"izmir-bakircay-universitesi": { ogrenci: 8576, ogretimUyesi: 340, akademisyen: 502 },
|
"izmir-bakircay-universitesi": { ogrenci: 8576, ogretimUyesi: 340, akademisyen: 502, kadin: 4095, erkek: 2964 },
|
||||||
"izmir-demokrasi-universitesi": { ogrenci: 11641, ogretimUyesi: 321, akademisyen: 450 },
|
"izmir-demokrasi-universitesi": { ogrenci: 11641, ogretimUyesi: 321, akademisyen: 450, kadin: 5995, erkek: 4944 },
|
||||||
"izmir-ekonomi-universitesi": { ogrenci: 11623, ogretimUyesi: 356, akademisyen: 626 },
|
"izmir-ekonomi-universitesi": { ogrenci: 11623, ogretimUyesi: 356, akademisyen: 626, kadin: 6000, erkek: 5106 },
|
||||||
"izmir-katip-celebi-universitesi": { ogrenci: 19148, ogretimUyesi: 778, akademisyen: 1215 },
|
"izmir-katip-celebi-universitesi": { ogrenci: 19148, ogretimUyesi: 778, akademisyen: 1215, kadin: 8539, erkek: 7813 },
|
||||||
"izmir-konak-meslek-yuksekokulu": { ogrenci: 3816, ogretimUyesi: 28, akademisyen: 116 },
|
"izmir-konak-meslek-yuksekokulu": { ogrenci: 3816, ogretimUyesi: 28, akademisyen: 116, kadin: 2097, erkek: 1719 },
|
||||||
"izmir-tinaztepe-universitesi": { ogrenci: 4685, ogretimUyesi: 199, akademisyen: 232 },
|
"izmir-tinaztepe-universitesi": { ogrenci: 4685, ogretimUyesi: 199, akademisyen: 232, kadin: 3436, erkek: 1111 },
|
||||||
"izmir-yuksek-teknoloji-enstitusu": { ogrenci: 7109, ogretimUyesi: 234, akademisyen: 685 },
|
"izmir-yuksek-teknoloji-enstitusu": { ogrenci: 7109, ogretimUyesi: 234, akademisyen: 685, kadin: 2696, erkek: 3170 },
|
||||||
"kadir-has-universitesi": { ogrenci: 5237, ogretimUyesi: 163, akademisyen: 257 },
|
"kadir-has-universitesi": { ogrenci: 5237, ogretimUyesi: 163, akademisyen: 257, kadin: 2654, erkek: 1926 },
|
||||||
"kafkas-universitesi": { ogrenci: 18892, ogretimUyesi: 712, akademisyen: 989 },
|
"kafkas-universitesi": { ogrenci: 18892, ogretimUyesi: 712, akademisyen: 989, kadin: 9477, erkek: 7736 },
|
||||||
"kahramanmaras-istiklal-universitesi": { ogrenci: 4080, ogretimUyesi: 104, akademisyen: 186 },
|
"kahramanmaras-istiklal-universitesi": { ogrenci: 4080, ogretimUyesi: 104, akademisyen: 186, kadin: 1771, erkek: 2208 },
|
||||||
"kahramanmaras-sutcu-imam-universitesi": { ogrenci: 30728, ogretimUyesi: 867, akademisyen: 1335 },
|
"kahramanmaras-sutcu-imam-universitesi": { ogrenci: 30728, ogretimUyesi: 867, akademisyen: 1335, kadin: 14430, erkek: 14124 },
|
||||||
"kapadokya-universitesi": { ogrenci: 10946, ogretimUyesi: 149, akademisyen: 276 },
|
"kapadokya-universitesi": { ogrenci: 10946, ogretimUyesi: 149, akademisyen: 276, kadin: 5715, erkek: 4525 },
|
||||||
"karabuk-universitesi": { ogrenci: 37533, ogretimUyesi: 808, akademisyen: 1179 },
|
"karabuk-universitesi": { ogrenci: 37533, ogretimUyesi: 808, akademisyen: 1179, kadin: 13816, erkek: 19911 },
|
||||||
"karadeniz-teknik-universitesi": { ogrenci: 28957, ogretimUyesi: 1198, akademisyen: 2144 },
|
"karadeniz-teknik-universitesi": { ogrenci: 28957, ogretimUyesi: 1198, akademisyen: 2144, kadin: 11736, erkek: 13659 },
|
||||||
"karamanoglu-mehmetbey-universitesi": { ogrenci: 19098, ogretimUyesi: 641, akademisyen: 908 },
|
"karamanoglu-mehmetbey-universitesi": { ogrenci: 19098, ogretimUyesi: 641, akademisyen: 908, kadin: 9829, erkek: 7281 },
|
||||||
"kastamonu-universitesi": { ogrenci: 25308, ogretimUyesi: 639, akademisyen: 980 },
|
"kastamonu-universitesi": { ogrenci: 25308, ogretimUyesi: 639, akademisyen: 980, kadin: 12535, erkek: 11192 },
|
||||||
"kayseri-universitesi": { ogrenci: 14389, ogretimUyesi: 174, akademisyen: 315 },
|
"kayseri-universitesi": { ogrenci: 14389, ogretimUyesi: 174, akademisyen: 315, kadin: 5906, erkek: 8025 },
|
||||||
"kilis-7-aralik-universitesi": { ogrenci: 13370, ogretimUyesi: 304, akademisyen: 478 },
|
"kilis-7-aralik-universitesi": { ogrenci: 13370, ogretimUyesi: 304, akademisyen: 478, kadin: 7332, erkek: 5302 },
|
||||||
"kirikkale-universitesi": { ogrenci: 29779, ogretimUyesi: 775, akademisyen: 1209 },
|
"kirikkale-universitesi": { ogrenci: 29779, ogretimUyesi: 775, akademisyen: 1209, kadin: 13579, erkek: 13193 },
|
||||||
"kirklareli-universitesi": { ogrenci: 22377, ogretimUyesi: 560, akademisyen: 915 },
|
"kirklareli-universitesi": { ogrenci: 22377, ogretimUyesi: 560, akademisyen: 915, kadin: 11206, erkek: 10284 },
|
||||||
"kirsehir-ahi-evran-universitesi": { ogrenci: 20182, ogretimUyesi: 670, akademisyen: 990 },
|
"kirsehir-ahi-evran-universitesi": { ogrenci: 20182, ogretimUyesi: 670, akademisyen: 990, kadin: 9812, erkek: 8915 },
|
||||||
"koc-universitesi": { ogrenci: 8596, ogretimUyesi: 432, akademisyen: 838 },
|
"koc-universitesi": { ogrenci: 8596, ogretimUyesi: 432, akademisyen: 838, kadin: 3308, erkek: 3834 },
|
||||||
"kocaeli-saglik-ve-teknoloji-universitesi": { ogrenci: 6271, ogretimUyesi: 115, akademisyen: 152 },
|
"kocaeli-saglik-ve-teknoloji-universitesi": { ogrenci: 6271, ogretimUyesi: 115, akademisyen: 152, kadin: 3400, erkek: 2611 },
|
||||||
"kocaeli-universitesi": { ogrenci: 53036, ogretimUyesi: 1191, akademisyen: 2174 },
|
"kocaeli-universitesi": { ogrenci: 53036, ogretimUyesi: 1191, akademisyen: 2174, kadin: 21178, erkek: 24858 },
|
||||||
"konya-gida-ve-tarim-universitesi": { ogrenci: 1614, ogretimUyesi: 36, akademisyen: 57 },
|
"konya-gida-ve-tarim-universitesi": { ogrenci: 1614, ogretimUyesi: 36, akademisyen: 57, kadin: 699, erkek: 607 },
|
||||||
"konya-teknik-universitesi": { ogrenci: 10688, ogretimUyesi: 312, akademisyen: 521 },
|
"konya-teknik-universitesi": { ogrenci: 10688, ogretimUyesi: 312, akademisyen: 521, kadin: 2574, erkek: 5847 },
|
||||||
"kto-karatay-universitesi": { ogrenci: 9768, ogretimUyesi: 211, akademisyen: 341 },
|
"kto-karatay-universitesi": { ogrenci: 9768, ogretimUyesi: 211, akademisyen: 341, kadin: 5445, erkek: 3531 },
|
||||||
"kutahya-dumlupinar-universitesi": { ogrenci: 38491, ogretimUyesi: 652, akademisyen: 1007 },
|
"kutahya-dumlupinar-universitesi": { ogrenci: 38491, ogretimUyesi: 652, akademisyen: 1007, kadin: 16742, erkek: 18326 },
|
||||||
"kutahya-saglik-bilimleri-universitesi": { ogrenci: 6757, ogretimUyesi: 326, akademisyen: 521 },
|
"kutahya-saglik-bilimleri-universitesi": { ogrenci: 6757, ogretimUyesi: 326, akademisyen: 521, kadin: 4791, erkek: 1824 },
|
||||||
"lokman-hekim-universitesi": { ogrenci: 5137, ogretimUyesi: 241, akademisyen: 331 },
|
"lokman-hekim-universitesi": { ogrenci: 5137, ogretimUyesi: 241, akademisyen: 331, kadin: 3509, erkek: 1412 },
|
||||||
"malatya-turgut-ozal-universitesi": { ogrenci: 12370, ogretimUyesi: 355, akademisyen: 510 },
|
"malatya-turgut-ozal-universitesi": { ogrenci: 12370, ogretimUyesi: 355, akademisyen: 510, kadin: 5756, erkek: 6016 },
|
||||||
"maltepe-universitesi": { ogrenci: 6288, ogretimUyesi: 370, akademisyen: 577 },
|
"maltepe-universitesi": { ogrenci: 6288, ogretimUyesi: 370, akademisyen: 577, kadin: 2914, erkek: 2901 },
|
||||||
"manisa-celal-bayar-universitesi": { ogrenci: 42218, ogretimUyesi: 986, akademisyen: 1647 },
|
"manisa-celal-bayar-universitesi": { ogrenci: 42218, ogretimUyesi: 986, akademisyen: 1647, kadin: 18597, erkek: 20366 },
|
||||||
"mardin-artuklu-universitesi": { ogrenci: 16705, ogretimUyesi: 456, akademisyen: 722 },
|
"mardin-artuklu-universitesi": { ogrenci: 16705, ogretimUyesi: 456, akademisyen: 722, kadin: 7591, erkek: 7108 },
|
||||||
"marmara-universitesi": { ogrenci: 56733, ogretimUyesi: 2168, akademisyen: 3410 },
|
"marmara-universitesi": { ogrenci: 56733, ogretimUyesi: 2168, akademisyen: 3410, kadin: 24398, erkek: 22729 },
|
||||||
"mef-universitesi": { ogrenci: 3708, ogretimUyesi: 115, akademisyen: 181 },
|
"mef-universitesi": { ogrenci: 3708, ogretimUyesi: 115, akademisyen: 181, kadin: 1997, erkek: 1554 },
|
||||||
"mersin-universitesi": { ogrenci: 37444, ogretimUyesi: 914, akademisyen: 1749 },
|
"mersin-universitesi": { ogrenci: 37444, ogretimUyesi: 914, akademisyen: 1749, kadin: 17040, erkek: 17154 },
|
||||||
"mimar-sinan-guzel-sanatlar-universitesi": { ogrenci: 8956, ogretimUyesi: 457, akademisyen: 631 },
|
"mimar-sinan-guzel-sanatlar-universitesi": { ogrenci: 8956, ogretimUyesi: 457, akademisyen: 631, kadin: 4901, erkek: 3136 },
|
||||||
"mudanya-universitesi": { ogrenci: 3040, ogretimUyesi: 98, akademisyen: 158 },
|
"mudanya-universitesi": { ogrenci: 3040, ogretimUyesi: 98, akademisyen: 158, kadin: 1859, erkek: 999 },
|
||||||
"mugla-sitki-kocman-universitesi": { ogrenci: 39185, ogretimUyesi: 991, akademisyen: 1727 },
|
"mugla-sitki-kocman-universitesi": { ogrenci: 39185, ogretimUyesi: 991, akademisyen: 1727, kadin: 18559, erkek: 18248 },
|
||||||
"munzur-universitesi": { ogrenci: 9826, ogretimUyesi: 318, akademisyen: 511 },
|
"munzur-universitesi": { ogrenci: 9826, ogretimUyesi: 318, akademisyen: 511, kadin: 5264, erkek: 3562 },
|
||||||
"mus-alparslan-universitesi": { ogrenci: 14993, ogretimUyesi: 418, akademisyen: 647 },
|
"mus-alparslan-universitesi": { ogrenci: 14993, ogretimUyesi: 418, akademisyen: 647, kadin: 7254, erkek: 6563 },
|
||||||
"necmettin-erbakan-universitesi": { ogrenci: 36239, ogretimUyesi: 1164, akademisyen: 1954 },
|
"necmettin-erbakan-universitesi": { ogrenci: 36239, ogretimUyesi: 1164, akademisyen: 1954, kadin: 17173, erkek: 13281 },
|
||||||
"nevsehir-haci-bektas-veli-universitesi": { ogrenci: 21073, ogretimUyesi: 471, akademisyen: 822 },
|
"nevsehir-haci-bektas-veli-universitesi": { ogrenci: 21073, ogretimUyesi: 471, akademisyen: 822, kadin: 11946, erkek: 7748 },
|
||||||
"nigde-omer-halisdemir-universitesi": { ogrenci: 26201, ogretimUyesi: 746, akademisyen: 1149 },
|
"nigde-omer-halisdemir-universitesi": { ogrenci: 26201, ogretimUyesi: 746, akademisyen: 1149, kadin: 12434, erkek: 11500 },
|
||||||
"nuh-naci-yazgan-universitesi": { ogrenci: 2902, ogretimUyesi: 98, akademisyen: 162 },
|
"nuh-naci-yazgan-universitesi": { ogrenci: 2902, ogretimUyesi: 98, akademisyen: 162, kadin: 1821, erkek: 952 },
|
||||||
"ondokuz-mayis-universitesi": { ogrenci: 44852, ogretimUyesi: 1253, akademisyen: 2305 },
|
"ondokuz-mayis-universitesi": { ogrenci: 44852, ogretimUyesi: 1253, akademisyen: 2305, kadin: 21720, erkek: 18555 },
|
||||||
"ordu-universitesi": { ogrenci: 17767, ogretimUyesi: 598, akademisyen: 973 },
|
"ordu-universitesi": { ogrenci: 17767, ogretimUyesi: 598, akademisyen: 973, kadin: 8453, erkek: 7543 },
|
||||||
"orta-dogu-teknik-universitesi": { ogrenci: 30055, ogretimUyesi: 757, akademisyen: 2058 },
|
"orta-dogu-teknik-universitesi": { ogrenci: 30055, ogretimUyesi: 757, akademisyen: 2058, kadin: 8820, erkek: 13089 },
|
||||||
"osmaniye-korkut-ata-universitesi": { ogrenci: 16460, ogretimUyesi: 365, akademisyen: 609 },
|
"osmaniye-korkut-ata-universitesi": { ogrenci: 16460, ogretimUyesi: 365, akademisyen: 609, kadin: 7770, erkek: 7478 },
|
||||||
"ostim-teknik-universitesi": { ogrenci: 8469, ogretimUyesi: 148, akademisyen: 257 },
|
"ostim-teknik-universitesi": { ogrenci: 8469, ogretimUyesi: 148, akademisyen: 257, kadin: 2235, erkek: 5740 },
|
||||||
"ozyegin-universitesi": { ogrenci: 8369, ogretimUyesi: 255, akademisyen: 465 },
|
"ozyegin-universitesi": { ogrenci: 8369, ogretimUyesi: 255, akademisyen: 465, kadin: 3607, erkek: 4032 },
|
||||||
"pamukkale-universitesi": { ogrenci: 43013, ogretimUyesi: 1238, akademisyen: 2113 },
|
"pamukkale-universitesi": { ogrenci: 43013, ogretimUyesi: 1238, akademisyen: 2113, kadin: 20835, erkek: 19216 },
|
||||||
"piri-reis-universitesi": { ogrenci: 6741, ogretimUyesi: 121, akademisyen: 217 },
|
"piri-reis-universitesi": { ogrenci: 6741, ogretimUyesi: 121, akademisyen: 217, kadin: 1706, erkek: 4859 },
|
||||||
"recep-tayyip-erdogan-universitesi": { ogrenci: 18693, ogretimUyesi: 703, akademisyen: 1252 },
|
"recep-tayyip-erdogan-universitesi": { ogrenci: 18693, ogretimUyesi: 703, akademisyen: 1252, kadin: 9135, erkek: 7622 },
|
||||||
"sabanci-universitesi": { ogrenci: 5093, ogretimUyesi: 212, akademisyen: 363 },
|
"sabanci-universitesi": { ogrenci: 5093, ogretimUyesi: 212, akademisyen: 363, kadin: 1586, erkek: 2540 },
|
||||||
"saglik-bilimleri-universitesi": { ogrenci: 25070, ogretimUyesi: 3992, akademisyen: 4397 },
|
"saglik-bilimleri-universitesi": { ogrenci: 25070, ogretimUyesi: 3992, akademisyen: 4397, kadin: 15532, erkek: 8165 },
|
||||||
"sakarya-universitesi": { ogrenci: 44027, ogretimUyesi: 1088, akademisyen: 1678 },
|
"sakarya-universitesi": { ogrenci: 44027, ogretimUyesi: 1088, akademisyen: 1678, kadin: 19421, erkek: 16279 },
|
||||||
"sakarya-uygulamali-bilimler-universitesi": { ogrenci: 29466, ogretimUyesi: 374, akademisyen: 698 },
|
"sakarya-uygulamali-bilimler-universitesi": { ogrenci: 29466, ogretimUyesi: 374, akademisyen: 698, kadin: 10819, erkek: 16785 },
|
||||||
"samsun-universitesi": { ogrenci: 8287, ogretimUyesi: 363, akademisyen: 515 },
|
"samsun-universitesi": { ogrenci: 8287, ogretimUyesi: 363, akademisyen: 515, kadin: 3672, erkek: 3972 },
|
||||||
"sanko-universitesi": { ogrenci: 1724, ogretimUyesi: 115, akademisyen: 167 },
|
"sanko-universitesi": { ogrenci: 1724, ogretimUyesi: 115, akademisyen: 167, kadin: 1138, erkek: 492 },
|
||||||
"selcuk-universitesi": { ogrenci: 61768, ogretimUyesi: 1486, akademisyen: 2594 },
|
"selcuk-universitesi": { ogrenci: 61768, ogretimUyesi: 1486, akademisyen: 2594, kadin: 29801, erkek: 25791 },
|
||||||
"siirt-universitesi": { ogrenci: 16165, ogretimUyesi: 482, akademisyen: 689 },
|
"siirt-universitesi": { ogrenci: 16165, ogretimUyesi: 482, akademisyen: 689, kadin: 7554, erkek: 7429 },
|
||||||
"sinop-universitesi": { ogrenci: 13767, ogretimUyesi: 421, akademisyen: 626 },
|
"sinop-universitesi": { ogrenci: 13767, ogretimUyesi: 421, akademisyen: 626, kadin: 6953, erkek: 5556 },
|
||||||
"sirnak-universitesi": { ogrenci: 6543, ogretimUyesi: 211, akademisyen: 330 },
|
"sirnak-universitesi": { ogrenci: 6543, ogretimUyesi: 211, akademisyen: 330, kadin: 2708, erkek: 2943 },
|
||||||
"sivas-bilim-ve-teknoloji-universitesi": { ogrenci: 940, ogretimUyesi: 81, akademisyen: 177 },
|
"sivas-bilim-ve-teknoloji-universitesi": { ogrenci: 940, ogretimUyesi: 81, akademisyen: 177, kadin: 289, erkek: 465 },
|
||||||
"sivas-cumhuriyet-universitesi": { ogrenci: 46095, ogretimUyesi: 1184, akademisyen: 1921 },
|
"sivas-cumhuriyet-universitesi": { ogrenci: 46095, ogretimUyesi: 1184, akademisyen: 1921, kadin: 22629, erkek: 19612 },
|
||||||
"suleyman-demirel-universitesi": { ogrenci: 33222, ogretimUyesi: 1097, akademisyen: 1847 },
|
"suleyman-demirel-universitesi": { ogrenci: 33222, ogretimUyesi: 1097, akademisyen: 1847, kadin: 17249, erkek: 13294 },
|
||||||
"tarsus-universitesi": { ogrenci: 7088, ogretimUyesi: 161, akademisyen: 285 },
|
"tarsus-universitesi": { ogrenci: 7088, ogretimUyesi: 161, akademisyen: 285, kadin: 3645, erkek: 2896 },
|
||||||
"ted-universitesi": { ogrenci: 5454, ogretimUyesi: 188, akademisyen: 331 },
|
"ted-universitesi": { ogrenci: 5454, ogretimUyesi: 188, akademisyen: 331, kadin: 3042, erkek: 2080 },
|
||||||
"tekirdag-namik-kemal-universitesi": { ogrenci: 28761, ogretimUyesi: 661, akademisyen: 1125 },
|
"tekirdag-namik-kemal-universitesi": { ogrenci: 28761, ogretimUyesi: 661, akademisyen: 1125, kadin: 12437, erkek: 14376 },
|
||||||
"tobb-ekonomi-ve-teknoloji-universitesi": { ogrenci: 5597, ogretimUyesi: 242, akademisyen: 378 },
|
"tobb-ekonomi-ve-teknoloji-universitesi": { ogrenci: 5597, ogretimUyesi: 242, akademisyen: 378, kadin: 2649, erkek: 2611 },
|
||||||
"tokat-gaziosmanpasa-universitesi": { ogrenci: 32387, ogretimUyesi: 964, akademisyen: 1617 },
|
"tokat-gaziosmanpasa-universitesi": { ogrenci: 32387, ogretimUyesi: 964, akademisyen: 1617, kadin: 15275, erkek: 13354 },
|
||||||
"toros-universitesi": { ogrenci: 4319, ogretimUyesi: 96, akademisyen: 155 },
|
"toros-universitesi": { ogrenci: 4319, ogretimUyesi: 96, akademisyen: 155, kadin: 2497, erkek: 1710 },
|
||||||
"trabzon-universitesi": { ogrenci: 11965, ogretimUyesi: 432, akademisyen: 665 },
|
"trabzon-universitesi": { ogrenci: 11965, ogretimUyesi: 432, akademisyen: 665, kadin: 6516, erkek: 4458 },
|
||||||
"trakya-universitesi": { ogrenci: 35922, ogretimUyesi: 933, akademisyen: 1710 },
|
"trakya-universitesi": { ogrenci: 35922, ogretimUyesi: 933, akademisyen: 1710, kadin: 17801, erkek: 16193 },
|
||||||
"turk-alman-universitesi": { ogrenci: 4574, ogretimUyesi: 126, akademisyen: 364 },
|
"turk-alman-universitesi": { ogrenci: 4574, ogretimUyesi: 126, akademisyen: 364, kadin: 1812, erkek: 2405 },
|
||||||
"turk-hava-kurumu-universitesi": { ogrenci: 4144, ogretimUyesi: 73, akademisyen: 162 },
|
"turk-hava-kurumu-universitesi": { ogrenci: 4144, ogretimUyesi: 73, akademisyen: 162, kadin: 1525, erkek: 2420 },
|
||||||
"ufuk-universitesi": { ogrenci: 4280, ogretimUyesi: 181, akademisyen: 284 },
|
"ufuk-universitesi": { ogrenci: 4280, ogretimUyesi: 181, akademisyen: 284, kadin: 2298, erkek: 1645 },
|
||||||
"usak-universitesi": { ogrenci: 22113, ogretimUyesi: 495, akademisyen: 930 },
|
"usak-universitesi": { ogrenci: 22113, ogretimUyesi: 495, akademisyen: 930, kadin: 11786, erkek: 9920 },
|
||||||
"uskudar-universitesi": { ogrenci: 23805, ogretimUyesi: 506, akademisyen: 702 },
|
"uskudar-universitesi": { ogrenci: 23805, ogretimUyesi: 506, akademisyen: 702, kadin: 12647, erkek: 7975 },
|
||||||
"van-yuzuncu-yil-universitesi": { ogrenci: 28497, ogretimUyesi: 1195, akademisyen: 1744 },
|
"van-yuzuncu-yil-universitesi": { ogrenci: 28497, ogretimUyesi: 1195, akademisyen: 1744, kadin: 12411, erkek: 12107 },
|
||||||
"yalova-universitesi": { ogrenci: 16528, ogretimUyesi: 451, akademisyen: 758 },
|
"yalova-universitesi": { ogrenci: 16528, ogretimUyesi: 451, akademisyen: 758, kadin: 7958, erkek: 7132 },
|
||||||
"yasar-universitesi": { ogrenci: 10081, ogretimUyesi: 226, akademisyen: 414 },
|
"yasar-universitesi": { ogrenci: 10081, ogretimUyesi: 226, akademisyen: 414, kadin: 4775, erkek: 4809 },
|
||||||
"yeditepe-universitesi": { ogrenci: 19371, ogretimUyesi: 602, akademisyen: 1126 },
|
"yeditepe-universitesi": { ogrenci: 19371, ogretimUyesi: 602, akademisyen: 1126, kadin: 9336, erkek: 8200 },
|
||||||
"yildiz-teknik-universitesi": { ogrenci: 36618, ogretimUyesi: 1072, akademisyen: 1801 },
|
"yildiz-teknik-universitesi": { ogrenci: 36618, ogretimUyesi: 1072, akademisyen: 1801, kadin: 11322, erkek: 14554 },
|
||||||
"yozgat-bozok-universitesi": { ogrenci: 23118, ogretimUyesi: 656, akademisyen: 1028 },
|
"yozgat-bozok-universitesi": { ogrenci: 23118, ogretimUyesi: 656, akademisyen: 1028, kadin: 10902, erkek: 10839 },
|
||||||
"yuksek-ihtisas-universitesi": { ogrenci: 3654, ogretimUyesi: 197, akademisyen: 272 },
|
"yuksek-ihtisas-universitesi": { ogrenci: 3654, ogretimUyesi: 197, akademisyen: 272, kadin: 2466, erkek: 1148 },
|
||||||
"zonguldak-bulent-ecevit-universitesi": { ogrenci: 29666, ogretimUyesi: 713, akademisyen: 1354 },
|
"zonguldak-bulent-ecevit-universitesi": { ogrenci: 29666, ogretimUyesi: 713, akademisyen: 1354, kadin: 14881, erkek: 12672 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Üniversite sayfa slug'ının YÖK İstatistik satırı; yoksa null. */
|
/** Üniversite sayfa slug'ının YÖK İstatistik satırı; yoksa null. */
|
||||||
@@ -240,3 +245,14 @@ export function uniIstatistikBul(slug: string): UniIstatistik | null {
|
|||||||
export function ogrenciBasinaUye(i: UniIstatistik): number {
|
export function ogrenciBasinaUye(i: UniIstatistik): number {
|
||||||
return Math.round(i.ogrenci / i.ogretimUyesi);
|
return Math.round(i.ogrenci / i.ogretimUyesi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Örgün ön lisans + lisans öğrencisinde kadın yüzdesi (0-100, tam sayı);
|
||||||
|
* iki sayı da sıfırsa null. Üniversite düzeyidir, bölüm düzeyinde veri yok —
|
||||||
|
* bilgi olarak gösterilir, filtre ya da öncelik yapılmaz.
|
||||||
|
*/
|
||||||
|
export function kadinYuzdesi(i: UniIstatistik): number | null {
|
||||||
|
const toplam = i.kadin + i.erkek;
|
||||||
|
if (toplam === 0) return null;
|
||||||
|
return Math.round((i.kadin / toplam) * 100);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user