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)
|
||||
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,
|
||||
"tag": "0004_shocking_caretaker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1790287503094,
|
||||
"tag": "0005_uneven_omega_red",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"lint": "eslint && pnpm tasarim:denetim",
|
||||
"ingest": "tsx scripts/ingest.ts",
|
||||
"refresh": "tsx scripts/refresh.ts",
|
||||
"ozellik": "tsx scripts/program-ozellik.ts",
|
||||
"detay": "tsx scripts/detay.ts",
|
||||
"yokatlas:goc": "tsx scripts/yokatlas-goc.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).
|
||||
# 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] = {}
|
||||
cinsiyet: dict[str, list[int]] = {}
|
||||
s = sayfa("ogrenci")
|
||||
for r in range(4, s.nrows):
|
||||
v = s.row_values(r)
|
||||
@@ -64,6 +67,9 @@ for r in range(4, s.nrows):
|
||||
if tur in ("BİRİNCİ Ö.", "İKİNCİ Ö."):
|
||||
k = sayfa_slug(ad)
|
||||
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.
|
||||
akademik: dict[str, tuple[int, int]] = {}
|
||||
@@ -86,7 +92,11 @@ for k in sorted(sayfalar):
|
||||
uye, toplam = akademik[k]
|
||||
if ogrenci[k] == 0 or uye == 0:
|
||||
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
|
||||
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
|
||||
// öğ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
|
||||
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
||||
|
||||
@@ -123,6 +134,10 @@ export type UniIstatistik = {{
|
||||
ogretimUyesi: number;
|
||||
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
||||
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> = {{
|
||||
@@ -138,5 +153,16 @@ export function uniIstatistikBul(slug: string): UniIstatistik | null {{
|
||||
export function ogrenciBasinaUye(i: UniIstatistik): number {{
|
||||
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")
|
||||
|
||||
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 {
|
||||
spendCreditForMessage,
|
||||
bedelsizMesajKaydet,
|
||||
grantCredits,
|
||||
krediBittiIsaretle,
|
||||
} from "@/lib/credits";
|
||||
import { anahtarVar, SORUMLULUK_REDDI } from "@/lib/ai/client";
|
||||
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 type { RaporSonuc } 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";
|
||||
|
||||
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;
|
||||
|
||||
function sistemPromptu(opts: {
|
||||
params: RaporParams;
|
||||
listeOzeti: string;
|
||||
kilitli?: boolean;
|
||||
profil?: DanismanProfili;
|
||||
}): string {
|
||||
const { params } = opts;
|
||||
const secimVar =
|
||||
@@ -50,7 +78,28 @@ KURALLAR:
|
||||
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 —
|
||||
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.
|
||||
- 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
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
parcalar.push(
|
||||
`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.
|
||||
// Geçmiş krediyi düşmeden ÖNCE okunur ki kullanıcı mesajı dahil olmasın;
|
||||
// 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)
|
||||
kullanicininRaporu(session.user.id),
|
||||
appDb
|
||||
@@ -169,6 +225,7 @@ export async function POST(request: NextRequest) {
|
||||
where: eq(schema.user.id, session.user.id),
|
||||
columns: { hasPaket: true },
|
||||
}),
|
||||
profilGetir(session.user.id),
|
||||
]);
|
||||
if (!rapor?.result) {
|
||||
return Response.json(
|
||||
@@ -178,8 +235,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
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
|
||||
const harcama = await spendCreditForMessage({
|
||||
const harcama = bedelsiz
|
||||
? await bedelsizMesajKaydet({ userId: session.user.id, content: mesaj, clientMessageId })
|
||||
: await spendCreditForMessage({
|
||||
userId: session.user.id,
|
||||
content: mesaj,
|
||||
clientMessageId,
|
||||
@@ -213,11 +276,21 @@ export async function POST(request: NextRequest) {
|
||||
].join("\n")
|
||||
: tamOzet;
|
||||
|
||||
const raporParams = rapor.params as RaporParams;
|
||||
const system = sistemPromptu({
|
||||
params: rapor.params as RaporParams,
|
||||
params: raporParams,
|
||||
listeOzeti,
|
||||
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)
|
||||
if (process.env.NODE_ENV !== "production" && !anahtarVar()) {
|
||||
@@ -253,12 +326,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (!anahtarVar()) {
|
||||
if (!bedelsiz) {
|
||||
await grantCredits({
|
||||
userId: session.user.id,
|
||||
delta: 1,
|
||||
reason: "refund",
|
||||
refId: kullaniciMesajId,
|
||||
});
|
||||
}
|
||||
console.error("[soru] Yapay Zeka anahtarı tanımlı değil");
|
||||
return Response.json(
|
||||
{
|
||||
@@ -286,10 +361,13 @@ export async function POST(request: NextRequest) {
|
||||
let metin = "";
|
||||
try {
|
||||
// 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,
|
||||
mesajlar,
|
||||
maxTokens: 1024,
|
||||
araclar: ARACLAR,
|
||||
calistir: aracCalistirici(aracBaglami),
|
||||
onArac: (ad) => console.info("[soru] araç:", ad),
|
||||
})) {
|
||||
metin += parca;
|
||||
controller.enqueue(encoder.encode(parca));
|
||||
@@ -319,6 +397,7 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
if (!bedelsiz) {
|
||||
await grantCredits({
|
||||
userId,
|
||||
delta: 1,
|
||||
@@ -326,6 +405,7 @@ export async function POST(request: NextRequest) {
|
||||
refId: kullaniciMesajId,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
console.error("[soru] stream hatası:", err);
|
||||
controller.error(err);
|
||||
}
|
||||
@@ -337,6 +417,8 @@ export async function POST(request: NextRequest) {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"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",
|
||||
});
|
||||
|
||||
// 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() {
|
||||
const baslik = "font-heading text-lg font-bold text-slate-900";
|
||||
return (
|
||||
<>
|
||||
<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ı
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Son güncelleme: 2 Ağustos 2026
|
||||
Son güncelleme: 25 Eylül 2026
|
||||
</p>
|
||||
</Parallax>
|
||||
|
||||
@@ -33,66 +41,187 @@ export default function GizlilikPage() {
|
||||
strength={4}
|
||||
className="mt-8 space-y-6 text-sm leading-7 text-slate-700"
|
||||
>
|
||||
<section>
|
||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||
Hangi verileri işliyoruz?
|
||||
</h2>
|
||||
<p className="mt-2">
|
||||
KolayTercih; e-posta adresini (hesap oluşturma ve giriş için), YKS
|
||||
başarı sıralamanı ve sihirbazda yaptığın seçimleri (tercih listeni
|
||||
üretmek için), danışman sohbetine yazdığın mesajları (sorularını
|
||||
yanıtlamak için) ve ödeme yaptığında sipariş kayıtlarını işler.
|
||||
Kart bilgilerin bize hiç ulaşmaz; ödeme iyzico altyapısında
|
||||
gerçekleşir.
|
||||
</p>
|
||||
<section className="rounded-2xl border border-slate-200 bg-slate-50 p-5">
|
||||
<h2 className={baslik}>Kısa özet</h2>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5">
|
||||
<li>
|
||||
Senden yalnız e-posta, başarı sıralaman, sihirbaz seçimlerin,
|
||||
danışmana yazdıkların ve danışmana söyleyip kaydettiğin
|
||||
tercihler alınır. Ad, kimlik numarası, adres, cinsiyet, sağlık
|
||||
ya da gelir bilgisi istenmez.
|
||||
</li>
|
||||
<li>
|
||||
Bunlar yalnız tercih listeni üretmek ve sorularını yanıtlamak
|
||||
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>
|
||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||
Ne için kullanıyoruz?
|
||||
</h2>
|
||||
<h2 className={baslik}>Veri sorumlusu</h2>
|
||||
<p className="mt-2">
|
||||
Verilerin yalnızca tercih önerileri üretmek, hesabını ve kredi
|
||||
bakiyeni yönetmek ve sana hizmetle ilgili e-posta (giriş
|
||||
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{" "}
|
||||
Bu sitedeki kişisel verilerin veri sorumlusu KolayTercih’tir
|
||||
[şirket unvanı ve adresi eklenecek]. Sorularını ve başvurularını{" "}
|
||||
<Link href="/iletisim" className="text-primary underline">
|
||||
iletişim sayfasındaki
|
||||
</Link>{" "}
|
||||
adresten bize yazabilirsin; talepler 30 gün içinde
|
||||
sonuçlandırılır.
|
||||
e-posta adresine iletebilirsin.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-heading text-lg font-bold text-slate-900">
|
||||
Çerezler ve analitik
|
||||
</h2>
|
||||
<h2 className={baslik}>Hangi verileri, ne için, hangi dayanakla işliyoruz?</h2>
|
||||
<ul className="mt-2 list-disc space-y-2 pl-5">
|
||||
<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">
|
||||
Yalnızca oturumunu açık tutmak için zorunlu çerezler kullanılır;
|
||||
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ı
|
||||
sıralaman analitiğe ham hâliyle
|
||||
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>
|
||||
</section>
|
||||
</Parallax>
|
||||
|
||||
@@ -46,6 +46,7 @@ import { URAP_DONEM, URAP_KAYNAK_URL, URAP_TOPLAM } from "@/lib/urap-siralama";
|
||||
import {
|
||||
YOK_ISTATISTIK_DONEM,
|
||||
YOK_ISTATISTIK_KAYNAK_URL,
|
||||
kadinYuzdesi,
|
||||
OGRENCI_BASINA_UYE_MEDYAN,
|
||||
ogrenciBasinaUye,
|
||||
} from "@/lib/yok-istatistik";
|
||||
@@ -219,10 +220,19 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
</dd>
|
||||
</div>
|
||||
) : 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 ? (
|
||||
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
||||
<dd className={orta}>{sayi(i.ogrenci)}</dd>
|
||||
<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>
|
||||
) : null;
|
||||
const oranKaresi = oranVar ? (
|
||||
@@ -375,7 +385,8 @@ function IstatistikKaynakSatiri({ uni }: { uni: UniDetay }) {
|
||||
{YOK_ISTATISTIK_DONEM}. Örgün öğrenci birinci ve ikinci öğretimdeki tüm
|
||||
düzeyleri (ön lisans–doktora) kapsar, uzaktan ve açık öğretim dahil
|
||||
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
|
||||
href={YOK_ISTATISTIK_KAYNAK_URL}
|
||||
target="_blank"
|
||||
|
||||
@@ -77,15 +77,17 @@ export async function GirisKarti({
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-6 text-center text-xs leading-relaxed text-slate-500">
|
||||
Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "}
|
||||
<Link href="/gizlilik" className="underline hover:text-slate-700">
|
||||
gizlilik metnindeki
|
||||
</Link>{" "}
|
||||
gibi işlenmesini ve{" "}
|
||||
{/* Aydınlatma "okudum" (bilgilendirme), koşullar "kabul": KVKK İlke
|
||||
Kararı 2026/347 aydınlatma ile rızayı ayırır (hukuk-ve-rakipler.md C.2/8). */}
|
||||
Giriş yaparak{" "}
|
||||
<Link href="/kosullar" className="underline hover:text-slate-700">
|
||||
kullanım koşullarını
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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")
|
||||
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
||||
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar — 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ı?"}
|
||||
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
|
||||
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")
|
||||
@@ -34,7 +35,7 @@ flowchart TD
|
||||
classDef hayal fill:#fee2e2,stroke:#ef4444,color:#991b1b
|
||||
classDef dengeli fill:#fef3c7,stroke:#f59e0b,color:#92400e
|
||||
classDef garanti fill:#d1fae5,stroke:#10b981,color:#065f46
|
||||
class H,Y havuz
|
||||
class H,Y,P havuz
|
||||
class G,U uyari
|
||||
class L1 hayal
|
||||
class L2 dengeli
|
||||
@@ -44,12 +45,14 @@ export function HuniSemasi() {
|
||||
return (
|
||||
<ElCizimiSema
|
||||
tanim={SEMA}
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; 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={
|
||||
<>
|
||||
Havuza yalnızca en son tercih kılavuzunda yer alan programlar girer;
|
||||
kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez. Her
|
||||
adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||
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">
|
||||
dışına asla çıkmadan
|
||||
</span>{" "}
|
||||
|
||||
@@ -114,15 +114,20 @@ export function MeraklisinaIcerik() {
|
||||
göre yeniden hesaplanarak listelenir; üçüncü adımdaki
|
||||
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
|
||||
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>{" "}
|
||||
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
||||
verilir. Havuza yalnızca{" "}
|
||||
<strong>en son tercih kılavuzunda yer alan</strong>{" "}
|
||||
programlar girer: kapanmış ya da kodu değiştiği için eski kaydı
|
||||
kalmış bir program sana önerilmez. Aşağıdaki şema akışın tamamını
|
||||
gösterir — havuz 24'ün altına düşerse filtrelerin nasıl
|
||||
gevşetildiği, yine de yetmezse listenin eldeki programlarla, sana
|
||||
gösterir — havuz 24'ün altına düşerse filtrelerin hangi
|
||||
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.
|
||||
</p>
|
||||
<HuniSemasi />
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type DanismanOzeti,
|
||||
type IlkTercihYorumu,
|
||||
type Mesaj,
|
||||
type ProfilSatiri,
|
||||
} from "./sohbet-client";
|
||||
import { RevizyonKutusu } from "./revizyon-kutusu";
|
||||
import {
|
||||
@@ -55,6 +56,7 @@ export function ListemGovde({
|
||||
kredi,
|
||||
raporKredi,
|
||||
sohbetGecmisi,
|
||||
danismanProfili,
|
||||
ozet,
|
||||
ilkYorum,
|
||||
kalanHak,
|
||||
@@ -76,6 +78,8 @@ export function ListemGovde({
|
||||
* kapıdan geçtiği için aynı bedeli gösterir. */
|
||||
raporKredi: number;
|
||||
sohbetGecmisi: Mesaj[];
|
||||
/** Danışmanın kaydettiği tercihler (sunucudan; çipler bununla açılır) */
|
||||
danismanProfili: ProfilSatiri[];
|
||||
ozet?: DanismanOzeti;
|
||||
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
||||
ilkYorum?: IlkTercihYorumu;
|
||||
@@ -124,6 +128,12 @@ export function ListemGovde({
|
||||
? "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`,
|
||||
];
|
||||
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) {
|
||||
satirlar.push(`İlgi alanların: ${params.kategoriler.join(", ")}`);
|
||||
}
|
||||
@@ -318,6 +328,7 @@ export function ListemGovde({
|
||||
<SohbetClient
|
||||
baslangicKredi={kredi}
|
||||
initialMesajlar={sohbetGecmisi}
|
||||
initialProfil={danismanProfili}
|
||||
ozet={ozet}
|
||||
ilkYorum={ilkYorum}
|
||||
bildikleri={danismaninBildikleri}
|
||||
@@ -329,6 +340,7 @@ export function ListemGovde({
|
||||
<SohbetClient
|
||||
baslangicKredi={kredi}
|
||||
initialMesajlar={sohbetGecmisi}
|
||||
initialProfil={danismanProfili}
|
||||
ozet={ozet}
|
||||
ilkYorum={ilkYorum}
|
||||
bildikleri={danismaninBildikleri}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
cerezdenTercihProfili,
|
||||
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
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 type {
|
||||
MaskeliRapor,
|
||||
@@ -53,10 +55,11 @@ export async function ListemIcerik({
|
||||
? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}`
|
||||
: "/listem";
|
||||
const session = await verifySession(geriYol);
|
||||
const [user, satir, sohbetGecmisi] = await Promise.all([
|
||||
const [user, satir, sohbetGecmisi, danismanProfili] = await Promise.all([
|
||||
getCurrentUser(),
|
||||
kullanicininRaporu(session.user.id),
|
||||
sohbetGecmisiGetir(session.user.id),
|
||||
profilGetir(session.user.id),
|
||||
]);
|
||||
if (!user) redirect(`/giris?callback=${encodeURIComponent(geriYol)}`);
|
||||
|
||||
@@ -171,6 +174,7 @@ export async function ListemIcerik({
|
||||
user.hasPaket ? 0 : RAPOR_KREDI
|
||||
}
|
||||
sohbetGecmisi={sohbetGecmisi as Mesaj[]}
|
||||
danismanProfili={profilSatirlari(danismanProfili)}
|
||||
ozet={danismanOzeti}
|
||||
ilkYorum={ilkYorum}
|
||||
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 { useRouter } from "next/navigation";
|
||||
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 { cn } from "@/lib/utils";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
import { DotsLoader } from "@/components/ui/loader";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
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 {
|
||||
id: string;
|
||||
@@ -123,8 +127,11 @@ export function SohbetClient({
|
||||
ozet,
|
||||
ilkYorum,
|
||||
bildikleri,
|
||||
initialProfil,
|
||||
}: {
|
||||
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 */
|
||||
initialMesajlar?: Mesaj[];
|
||||
/** Boyutlandırma override'ı (ör. alt popup'ta h-full) */
|
||||
@@ -146,8 +153,57 @@ export function SohbetClient({
|
||||
const [kredi, setKredi] = useState(baslangicKredi);
|
||||
const [temizleAcik, setTemizleAcik] = 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();
|
||||
|
||||
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ı
|
||||
// (veya geçmiş mesajlar) çizilirken lazy sınırında düz-metin flash'ı olmasın.
|
||||
useEffect(() => {
|
||||
@@ -213,7 +269,8 @@ export function SohbetClient({
|
||||
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)
|
||||
olay("danisman_sorusu", {
|
||||
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
|
||||
// harcamasını görmez; bakiye orada da güncellensin.
|
||||
router.refresh();
|
||||
// Danışman bu cevapta profil_guncelle çağırmış olabilir
|
||||
void profiliCek(new Set(profil.map((s) => s.alan)));
|
||||
} catch {
|
||||
// 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
|
||||
@@ -358,6 +417,35 @@ export function SohbetClient({
|
||||
</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}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -576,6 +664,11 @@ export function SohbetClient({
|
||||
</PromptInputAction>
|
||||
</PromptInputActions>
|
||||
</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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
uretimKilidiBirak,
|
||||
} from "@/lib/uretim-kilidi";
|
||||
import { getSession, getCurrentUser } from "@/lib/session";
|
||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||
import { appDb, schema } from "@/lib/appdb";
|
||||
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
|
||||
import { PUAN_TURLERI, type PuanTuruKey } from "@/types/yokatlas";
|
||||
@@ -214,7 +215,9 @@ async function listeOlusturKilitli(
|
||||
|
||||
let sonuc: RaporSonuc;
|
||||
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) {
|
||||
// Ü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
|
||||
@@ -347,10 +350,12 @@ export async function listeRevize(input: {
|
||||
|
||||
let sonuc: RaporSonuc;
|
||||
try {
|
||||
const profil = await profilGetir(user.id).catch(() => undefined);
|
||||
sonuc = await raporUret(
|
||||
params,
|
||||
feedback.slice(0, 1000),
|
||||
listeOzetiCikar(onceki, params.sira),
|
||||
profil,
|
||||
);
|
||||
} catch (err) {
|
||||
// İ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 { 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.
|
||||
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";
|
||||
|
||||
// Tüm AI akışı OpenRouter üzerinden tek kapıdan gider (kullanıcı kararı,
|
||||
// 7 Ağu 2026 — önceki Anthropic/Gemini/lokal sağlayıcı seçimi kaldırıldı).
|
||||
// Model .env'den değiştirilebilir; rapor ve sohbet istenirse ayrışabilir.
|
||||
// Tüm AI akışı OpenAI uyumlu TEK kapıdan gider. Varsayılan OpenRouter
|
||||
// (7 Ağu 2026 kararı); 26 Eyl 2026 kararıyla model Türkiye'de barındırılacak:
|
||||
// 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_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 {
|
||||
return (
|
||||
@@ -23,20 +43,20 @@ export function sohbetModeli(): string {
|
||||
|
||||
/** API anahtarı tanımlı mı (mock moduna düşülecek mi)? */
|
||||
export function anahtarVar(): boolean {
|
||||
return Boolean(process.env.OPENROUTER_API_KEY);
|
||||
return Boolean(apiKey());
|
||||
}
|
||||
|
||||
const globalForAi = globalThis as unknown as { __openrouter?: OpenAI };
|
||||
|
||||
export function getOpenRouter(): OpenAI {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) {
|
||||
const key = apiKey();
|
||||
if (!key) {
|
||||
throw new Error("AI_KEY_MISSING");
|
||||
}
|
||||
if (!globalForAi.__openrouter) {
|
||||
globalForAi.__openrouter = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey,
|
||||
baseURL: baseUrl(),
|
||||
apiKey: key,
|
||||
// Tek retry otoritesi cagri.ts'teki geciciHatadaTekrarla
|
||||
maxRetries: 0,
|
||||
// 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 { z } from "zod";
|
||||
import type {
|
||||
@@ -231,8 +232,10 @@ export async function raporUret(
|
||||
params: RaporParams,
|
||||
revizyonNotu?: string,
|
||||
oncekiListeOzeti?: string,
|
||||
profil?: DanismanProfili,
|
||||
): Promise<RaporSonuc> {
|
||||
const { havuz, gevsetildi } = havuzOlustur(params);
|
||||
const { havuz, gevsetildi } = havuzOlustur(params, profil);
|
||||
const profilSatir = profil ? profilSatirlari(profil) : [];
|
||||
if (havuz.length < MIN_LISTE) {
|
||||
throw new RaporUretimHatasi(
|
||||
"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");
|
||||
|
||||
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")) {
|
||||
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.`,
|
||||
@@ -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.`
|
||||
: 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,
|
||||
revizyonNotu
|
||||
? `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ı
|
||||
// tıklama, Vazgeç); tamamlanan kapıda atılmaz.
|
||||
| "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>;
|
||||
|
||||
|
||||
@@ -231,3 +231,15 @@ export const savedLists = sqliteTable("saved_lists", {
|
||||
createdAt: integer("created_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. */
|
||||
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ü
|
||||
* 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[] } {
|
||||
if (grup === "devlet") return { sql: "AND unitur = ?", args: ["DEVLET"] };
|
||||
if (grup === "vakif")
|
||||
@@ -112,6 +154,7 @@ export function dilimAra(
|
||||
il?: string;
|
||||
iller?: string[];
|
||||
uniturGrubu?: UniturGrubu;
|
||||
ozellik?: OzellikSuzgeci;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {}
|
||||
@@ -132,16 +175,17 @@ export function dilimAra(
|
||||
? `AND il IN (${illerListe.map(() => "?").join(",")})`
|
||||
: "";
|
||||
const unitur = uniturFiltreSql(opts.uniturGrubu);
|
||||
const ozellik = ozellikFiltreSql(opts.ozellik);
|
||||
|
||||
const { alt, ust } = dilimAraligi(sira, dilim);
|
||||
if (ust != null && ust < alt) return { programlar: [], toplam: 0 };
|
||||
const siraKosulu =
|
||||
ust != null ? `${EFEKTIF_SIRA} BETWEEN ? AND ?` : `${EFEKTIF_SIRA} >= ?`;
|
||||
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
|
||||
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql}
|
||||
WHERE tur = ? AND onlisans = ? ${ilFiltre} ${unitur.sql} ${ozellik.sql}
|
||||
AND ${EFEKTIF_SIRA} IS NOT NULL
|
||||
AND ${siraKosulu}`;
|
||||
// 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;
|
||||
iller?: string[];
|
||||
uniturGrubu?: UniturGrubu;
|
||||
ozellik?: OzellikSuzgeci;
|
||||
limitPerBucket?: number;
|
||||
} = {}
|
||||
): RankResults {
|
||||
@@ -175,6 +220,7 @@ export function searchByRank(
|
||||
il: opts.il,
|
||||
iller: opts.iller,
|
||||
uniturGrubu: opts.uniturGrubu,
|
||||
ozellik: opts.ozellik,
|
||||
limit: opts.limitPerBucket ?? 12,
|
||||
};
|
||||
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
|
||||
// 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 {
|
||||
PUAN_TURLERI,
|
||||
type Program,
|
||||
@@ -16,10 +18,55 @@ export interface AdayProgram extends Program {
|
||||
dilim: Dilim;
|
||||
}
|
||||
|
||||
export type Gevsetilen = "profil" | "kategori" | "il";
|
||||
|
||||
export interface HavuzSonuc {
|
||||
havuz: AdayProgram[];
|
||||
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi
|
||||
gevsetildi: ("kategori" | "il")[];
|
||||
// Havuz 24'e ulaşmak için hangi filtreler gevşetildi (sırayla)
|
||||
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;
|
||||
@@ -89,12 +136,16 @@ function dilimEkle(r: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministik aday havuzu: Claude bu havuzdan SEÇER, asla uydurmaz.
|
||||
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) göre filtreler; havuz
|
||||
* 24'ün altına düşerse kademeli gevşetir (önce il, sonra kategori) ki liste
|
||||
* her zaman kurulabilsin.
|
||||
* Deterministik aday havuzu: yapay zekâ bu havuzdan SEÇER, asla uydurmaz.
|
||||
* Sihirbaz seçimlerine (kategori/il/üniversite tipi) ve varsa danışman
|
||||
* profiline (dil, öğretim türü, burs, ücret, km) göre filtreler; havuz 24'ün
|
||||
* 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 =
|
||||
params.universiteTipi && params.universiteTipi !== "farketmez"
|
||||
? params.universiteTipi
|
||||
@@ -110,6 +161,30 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
||||
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
|
||||
: 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)
|
||||
const tamHam = dilimEkle(
|
||||
searchByRank(params.sira, params.tur, {
|
||||
@@ -122,7 +197,7 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
||||
if (tamFiltreli.length >= HEDEF) {
|
||||
return {
|
||||
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) {
|
||||
return {
|
||||
havuz: dilimBasinaKirp(tamHam, PROMPT_LIMIT_DILIM),
|
||||
gevsetildi: ["kategori"],
|
||||
gevsetildi: [...profilGevsetildi, "kategori"],
|
||||
};
|
||||
}
|
||||
|
||||
// 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");
|
||||
const ilsizHam = dilimEkle(
|
||||
searchByRank(params.sira, params.tur, {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//
|
||||
// Öğ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.
|
||||
// Kadın/erkek = örgün ön lisans + lisans öğrencisi (E/K sütunları).
|
||||
// Anahtar: /universite/[slug] sayfa slug'ı. Burada olmayan üniversitenin
|
||||
// YÖK İstatistik satırı yok (KKTC/yurt dışı ya da öğrencisi/üyesi 0).
|
||||
|
||||
@@ -22,213 +23,217 @@ export type UniIstatistik = {
|
||||
ogretimUyesi: number;
|
||||
/** Tüm akademik personel (öğretim görevlisi ve araştırma görevlisi dahil) */
|
||||
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> = {
|
||||
"abdullah-gul-universitesi": { ogrenci: 3936, ogretimUyesi: 107, akademisyen: 300 },
|
||||
"acibadem-mehmet-ali-aydinlar-universitesi": { ogrenci: 6279, ogretimUyesi: 375, akademisyen: 646 },
|
||||
"adana-alparslan-turkes-bilim-ve-teknoloji-universitesi": { ogrenci: 6220, ogretimUyesi: 236, akademisyen: 446 },
|
||||
"adiyaman-universitesi": { ogrenci: 16659, ogretimUyesi: 548, akademisyen: 862 },
|
||||
"afyon-kocatepe-universitesi": { ogrenci: 29711, ogretimUyesi: 699, akademisyen: 1109 },
|
||||
"afyonkarahisar-saglik-bilimleri-universitesi": { ogrenci: 7072, ogretimUyesi: 325, akademisyen: 620 },
|
||||
"agri-ibrahim-cecen-universitesi": { ogrenci: 15239, ogretimUyesi: 390, akademisyen: 574 },
|
||||
"akdeniz-universitesi": { ogrenci: 55992, ogretimUyesi: 1339, akademisyen: 2658 },
|
||||
"aksaray-universitesi": { ogrenci: 18151, ogretimUyesi: 626, akademisyen: 963 },
|
||||
"alanya-alaaddin-keykubat-universitesi": { ogrenci: 15322, ogretimUyesi: 410, akademisyen: 696 },
|
||||
"alanya-universitesi": { ogrenci: 1207, ogretimUyesi: 51, akademisyen: 100 },
|
||||
"altinbas-universitesi": { ogrenci: 12055, ogretimUyesi: 227, akademisyen: 334 },
|
||||
"amasya-universitesi": { ogrenci: 16656, ogretimUyesi: 543, akademisyen: 748 },
|
||||
"anadolu-universitesi": { ogrenci: 25934, ogretimUyesi: 900, akademisyen: 1470 },
|
||||
"ankara-bilim-universitesi": { ogrenci: 3626, ogretimUyesi: 72, akademisyen: 134 },
|
||||
"ankara-haci-bayram-veli-universitesi": { ogrenci: 27559, ogretimUyesi: 867, akademisyen: 1221 },
|
||||
"ankara-medipol-universitesi": { ogrenci: 14035, ogretimUyesi: 270, akademisyen: 458 },
|
||||
"ankara-muzik-ve-guzel-sanatlar-universitesi": { ogrenci: 1457, ogretimUyesi: 86, akademisyen: 136 },
|
||||
"ankara-sosyal-bilimler-universitesi": { ogrenci: 5879, ogretimUyesi: 274, akademisyen: 477 },
|
||||
"ankara-universitesi": { ogrenci: 56128, ogretimUyesi: 2225, akademisyen: 3702 },
|
||||
"ankara-yildirim-beyazit-universitesi": { ogrenci: 26177, ogretimUyesi: 1019, akademisyen: 1568 },
|
||||
"antalya-belek-universitesi": { ogrenci: 2553, ogretimUyesi: 44, akademisyen: 94 },
|
||||
"antalya-bilim-universitesi": { ogrenci: 8597, ogretimUyesi: 180, akademisyen: 315 },
|
||||
"ardahan-universitesi": { ogrenci: 7355, ogretimUyesi: 236, akademisyen: 380 },
|
||||
"artvin-coruh-universitesi": { ogrenci: 12358, ogretimUyesi: 332, akademisyen: 566 },
|
||||
"atasehir-adiguzel-meslek-yuksekokulu": { ogrenci: 1476, ogretimUyesi: 2, akademisyen: 32 },
|
||||
"ataturk-universitesi": { ogrenci: 58279, ogretimUyesi: 1800, akademisyen: 2758 },
|
||||
"atilim-universitesi": { ogrenci: 9885, ogretimUyesi: 331, akademisyen: 594 },
|
||||
"avrasya-universitesi": { ogrenci: 7495, ogretimUyesi: 119, akademisyen: 238 },
|
||||
"aydin-adnan-menderes-universitesi": { ogrenci: 43686, ogretimUyesi: 1232, akademisyen: 1924 },
|
||||
"bahcesehir-universitesi": { ogrenci: 21181, ogretimUyesi: 560, akademisyen: 838 },
|
||||
"balikesir-universitesi": { ogrenci: 28673, ogretimUyesi: 890, akademisyen: 1313 },
|
||||
"bandirma-onyedi-eylul-universitesi": { ogrenci: 18949, ogretimUyesi: 456, akademisyen: 653 },
|
||||
"bartin-universitesi": { ogrenci: 15615, ogretimUyesi: 452, akademisyen: 723 },
|
||||
"baskent-universitesi": { ogrenci: 17784, ogretimUyesi: 976, akademisyen: 1920 },
|
||||
"batman-universitesi": { ogrenci: 14701, ogretimUyesi: 360, akademisyen: 584 },
|
||||
"bayburt-universitesi": { ogrenci: 13883, ogretimUyesi: 314, akademisyen: 499 },
|
||||
"beykoz-universitesi": { ogrenci: 5251, ogretimUyesi: 94, akademisyen: 154 },
|
||||
"bezm-i-alem-vakif-universitesi": { ogrenci: 4076, ogretimUyesi: 231, akademisyen: 605 },
|
||||
"bilecik-seyh-edebali-universitesi": { ogrenci: 19612, ogretimUyesi: 495, akademisyen: 824 },
|
||||
"bingol-universitesi": { ogrenci: 18558, ogretimUyesi: 494, akademisyen: 744 },
|
||||
"biruni-universitesi": { ogrenci: 15154, ogretimUyesi: 347, akademisyen: 449 },
|
||||
"bitlis-eren-universitesi": { ogrenci: 14635, ogretimUyesi: 342, akademisyen: 549 },
|
||||
"bogazici-universitesi": { ogrenci: 13839, ogretimUyesi: 503, akademisyen: 1031 },
|
||||
"bolu-abant-izzet-baysal-universitesi": { ogrenci: 30443, ogretimUyesi: 965, akademisyen: 1491 },
|
||||
"burdur-mehmet-akif-ersoy-universitesi": { ogrenci: 34361, ogretimUyesi: 799, akademisyen: 1209 },
|
||||
"bursa-teknik-universitesi": { ogrenci: 12346, ogretimUyesi: 350, akademisyen: 601 },
|
||||
"bursa-uludag-universitesi": { ogrenci: 58341, ogretimUyesi: 1368, akademisyen: 2331 },
|
||||
"cag-universitesi": { ogrenci: 3667, ogretimUyesi: 63, akademisyen: 127 },
|
||||
"canakkale-onsekiz-mart-universitesi": { ogrenci: 42341, ogretimUyesi: 1254, akademisyen: 2058 },
|
||||
"cankaya-universitesi": { ogrenci: 6401, ogretimUyesi: 148, akademisyen: 260 },
|
||||
"cankiri-karatekin-universitesi": { ogrenci: 17291, ogretimUyesi: 488, akademisyen: 740 },
|
||||
"cukurova-universitesi": { ogrenci: 44430, ogretimUyesi: 1187, akademisyen: 2116 },
|
||||
"demiroglu-bilim-universitesi": { ogrenci: 2633, ogretimUyesi: 179, akademisyen: 231 },
|
||||
"dicle-universitesi": { ogrenci: 42368, ogretimUyesi: 1011, akademisyen: 1641 },
|
||||
"dogus-universitesi": { ogrenci: 18442, ogretimUyesi: 217, akademisyen: 459 },
|
||||
"dokuz-eylul-universitesi": { ogrenci: 56833, ogretimUyesi: 1849, akademisyen: 2951 },
|
||||
"duzce-universitesi": { ogrenci: 28561, ogretimUyesi: 765, akademisyen: 1230 },
|
||||
"ege-universitesi": { ogrenci: 49852, ogretimUyesi: 1744, akademisyen: 2983 },
|
||||
"erciyes-universitesi": { ogrenci: 42735, ogretimUyesi: 1237, akademisyen: 2195 },
|
||||
"erzincan-binali-yildirim-universitesi": { ogrenci: 23617, ogretimUyesi: 764, akademisyen: 1070 },
|
||||
"erzurum-teknik-universitesi": { ogrenci: 5876, ogretimUyesi: 268, akademisyen: 389 },
|
||||
"eskisehir-osmangazi-universitesi": { ogrenci: 26667, ogretimUyesi: 1043, akademisyen: 1695 },
|
||||
"eskisehir-teknik-universitesi": { ogrenci: 13840, ogretimUyesi: 467, akademisyen: 735 },
|
||||
"fatih-sultan-mehmet-vakif-universitesi": { ogrenci: 7676, ogretimUyesi: 169, akademisyen: 328 },
|
||||
"fenerbahce-universitesi": { ogrenci: 9741, ogretimUyesi: 195, akademisyen: 311 },
|
||||
"firat-universitesi": { ogrenci: 39181, ogretimUyesi: 1167, akademisyen: 1924 },
|
||||
"galatasaray-universitesi": { ogrenci: 4398, ogretimUyesi: 162, akademisyen: 310 },
|
||||
"gazi-universitesi": { ogrenci: 38023, ogretimUyesi: 1877, akademisyen: 3115 },
|
||||
"gaziantep-islam-bilim-ve-teknoloji-universitesi": { ogrenci: 5934, ogretimUyesi: 167, akademisyen: 248 },
|
||||
"gaziantep-universitesi": { ogrenci: 45986, ogretimUyesi: 906, akademisyen: 1689 },
|
||||
"gebze-teknik-universitesi": { ogrenci: 10152, ogretimUyesi: 354, akademisyen: 820 },
|
||||
"giresun-universitesi": { ogrenci: 24556, ogretimUyesi: 820, akademisyen: 1165 },
|
||||
"gumushane-universitesi": { ogrenci: 19558, ogretimUyesi: 425, akademisyen: 664 },
|
||||
"hacettepe-universitesi": { ogrenci: 45592, ogretimUyesi: 2015, akademisyen: 3536 },
|
||||
"hakkari-universitesi": { ogrenci: 5255, ogretimUyesi: 216, akademisyen: 332 },
|
||||
"halic-universitesi": { ogrenci: 15757, ogretimUyesi: 256, akademisyen: 470 },
|
||||
"harran-universitesi": { ogrenci: 28476, ogretimUyesi: 740, akademisyen: 1249 },
|
||||
"hasan-kalyoncu-universitesi": { ogrenci: 7697, ogretimUyesi: 206, akademisyen: 311 },
|
||||
"hatay-mustafa-kemal-universitesi": { ogrenci: 18471, ogretimUyesi: 685, akademisyen: 1146 },
|
||||
"hitit-universitesi": { ogrenci: 18433, ogretimUyesi: 492, akademisyen: 821 },
|
||||
"ibn-haldun-universitesi": { ogrenci: 1877, ogretimUyesi: 110, akademisyen: 171 },
|
||||
"igdir-universitesi": { ogrenci: 13947, ogretimUyesi: 314, akademisyen: 491 },
|
||||
"ihsan-dogramaci-bilkent-universitesi": { ogrenci: 12244, ogretimUyesi: 367, akademisyen: 707 },
|
||||
"inonu-universitesi": { ogrenci: 30854, ogretimUyesi: 948, akademisyen: 1584 },
|
||||
"isik-universitesi": { ogrenci: 5999, ogretimUyesi: 108, akademisyen: 189 },
|
||||
"iskenderun-teknik-universitesi": { ogrenci: 13168, ogretimUyesi: 263, akademisyen: 388 },
|
||||
"isparta-uygulamali-bilimler-universitesi": { ogrenci: 22373, ogretimUyesi: 415, akademisyen: 739 },
|
||||
"istanbul-29-mayis-universitesi": { ogrenci: 5492, ogretimUyesi: 129, akademisyen: 187 },
|
||||
"istanbul-arel-universitesi": { ogrenci: 14076, ogretimUyesi: 276, akademisyen: 519 },
|
||||
"istanbul-atlas-universitesi": { ogrenci: 10704, ogretimUyesi: 376, akademisyen: 501 },
|
||||
"istanbul-aydin-universitesi": { ogrenci: 47200, ogretimUyesi: 777, akademisyen: 1200 },
|
||||
"istanbul-beykent-universitesi": { ogrenci: 26693, ogretimUyesi: 501, akademisyen: 824 },
|
||||
"istanbul-bilgi-universitesi": { ogrenci: 19216, ogretimUyesi: 310, akademisyen: 568 },
|
||||
"istanbul-esenyurt-universitesi": { ogrenci: 14673, ogretimUyesi: 150, akademisyen: 320 },
|
||||
"istanbul-galata-universitesi": { ogrenci: 4213, ogretimUyesi: 125, akademisyen: 192 },
|
||||
"istanbul-gedik-universitesi": { ogrenci: 10243, ogretimUyesi: 175, akademisyen: 306 },
|
||||
"istanbul-gelisim-universitesi": { ogrenci: 36730, ogretimUyesi: 472, akademisyen: 775 },
|
||||
"istanbul-kent-universitesi": { ogrenci: 15262, ogretimUyesi: 231, akademisyen: 405 },
|
||||
"istanbul-kultur-universitesi": { ogrenci: 13322, ogretimUyesi: 268, akademisyen: 459 },
|
||||
"istanbul-medeniyet-universitesi": { ogrenci: 15978, ogretimUyesi: 746, akademisyen: 1141 },
|
||||
"istanbul-medipol-universitesi": { ogrenci: 49066, ogretimUyesi: 975, akademisyen: 1647 },
|
||||
"istanbul-nisantasi-universitesi": { ogrenci: 42789, ogretimUyesi: 485, akademisyen: 783 },
|
||||
"istanbul-okan-universitesi": { ogrenci: 17022, ogretimUyesi: 560, akademisyen: 823 },
|
||||
"istanbul-rumeli-universitesi": { ogrenci: 10118, ogretimUyesi: 165, akademisyen: 257 },
|
||||
"istanbul-sabahattin-zaim-universitesi": { ogrenci: 8740, ogretimUyesi: 261, akademisyen: 392 },
|
||||
"istanbul-saglik-ve-teknoloji-universitesi": { ogrenci: 5136, ogretimUyesi: 350, akademisyen: 434 },
|
||||
"istanbul-sisli-meslek-yuksekokulu": { ogrenci: 10446, ogretimUyesi: 19, akademisyen: 80 },
|
||||
"istanbul-teknik-universitesi": { ogrenci: 33900, ogretimUyesi: 1253, akademisyen: 2524 },
|
||||
"istanbul-ticaret-universitesi": { ogrenci: 8384, ogretimUyesi: 167, akademisyen: 270 },
|
||||
"istanbul-topkapi-universitesi": { ogrenci: 24477, ogretimUyesi: 237, akademisyen: 424 },
|
||||
"istanbul-universitesi": { ogrenci: 52770, ogretimUyesi: 2125, akademisyen: 3379 },
|
||||
"istanbul-universitesi-cerrahpasa": { ogrenci: 29490, ogretimUyesi: 1318, akademisyen: 2053 },
|
||||
"istanbul-yeni-yuzyil-universitesi": { ogrenci: 20511, ogretimUyesi: 271, akademisyen: 424 },
|
||||
"istinye-universitesi": { ogrenci: 17697, ogretimUyesi: 525, akademisyen: 745 },
|
||||
"izmir-bakircay-universitesi": { ogrenci: 8576, ogretimUyesi: 340, akademisyen: 502 },
|
||||
"izmir-demokrasi-universitesi": { ogrenci: 11641, ogretimUyesi: 321, akademisyen: 450 },
|
||||
"izmir-ekonomi-universitesi": { ogrenci: 11623, ogretimUyesi: 356, akademisyen: 626 },
|
||||
"izmir-katip-celebi-universitesi": { ogrenci: 19148, ogretimUyesi: 778, akademisyen: 1215 },
|
||||
"izmir-konak-meslek-yuksekokulu": { ogrenci: 3816, ogretimUyesi: 28, akademisyen: 116 },
|
||||
"izmir-tinaztepe-universitesi": { ogrenci: 4685, ogretimUyesi: 199, akademisyen: 232 },
|
||||
"izmir-yuksek-teknoloji-enstitusu": { ogrenci: 7109, ogretimUyesi: 234, akademisyen: 685 },
|
||||
"kadir-has-universitesi": { ogrenci: 5237, ogretimUyesi: 163, akademisyen: 257 },
|
||||
"kafkas-universitesi": { ogrenci: 18892, ogretimUyesi: 712, akademisyen: 989 },
|
||||
"kahramanmaras-istiklal-universitesi": { ogrenci: 4080, ogretimUyesi: 104, akademisyen: 186 },
|
||||
"kahramanmaras-sutcu-imam-universitesi": { ogrenci: 30728, ogretimUyesi: 867, akademisyen: 1335 },
|
||||
"kapadokya-universitesi": { ogrenci: 10946, ogretimUyesi: 149, akademisyen: 276 },
|
||||
"karabuk-universitesi": { ogrenci: 37533, ogretimUyesi: 808, akademisyen: 1179 },
|
||||
"karadeniz-teknik-universitesi": { ogrenci: 28957, ogretimUyesi: 1198, akademisyen: 2144 },
|
||||
"karamanoglu-mehmetbey-universitesi": { ogrenci: 19098, ogretimUyesi: 641, akademisyen: 908 },
|
||||
"kastamonu-universitesi": { ogrenci: 25308, ogretimUyesi: 639, akademisyen: 980 },
|
||||
"kayseri-universitesi": { ogrenci: 14389, ogretimUyesi: 174, akademisyen: 315 },
|
||||
"kilis-7-aralik-universitesi": { ogrenci: 13370, ogretimUyesi: 304, akademisyen: 478 },
|
||||
"kirikkale-universitesi": { ogrenci: 29779, ogretimUyesi: 775, akademisyen: 1209 },
|
||||
"kirklareli-universitesi": { ogrenci: 22377, ogretimUyesi: 560, akademisyen: 915 },
|
||||
"kirsehir-ahi-evran-universitesi": { ogrenci: 20182, ogretimUyesi: 670, akademisyen: 990 },
|
||||
"koc-universitesi": { ogrenci: 8596, ogretimUyesi: 432, akademisyen: 838 },
|
||||
"kocaeli-saglik-ve-teknoloji-universitesi": { ogrenci: 6271, ogretimUyesi: 115, akademisyen: 152 },
|
||||
"kocaeli-universitesi": { ogrenci: 53036, ogretimUyesi: 1191, akademisyen: 2174 },
|
||||
"konya-gida-ve-tarim-universitesi": { ogrenci: 1614, ogretimUyesi: 36, akademisyen: 57 },
|
||||
"konya-teknik-universitesi": { ogrenci: 10688, ogretimUyesi: 312, akademisyen: 521 },
|
||||
"kto-karatay-universitesi": { ogrenci: 9768, ogretimUyesi: 211, akademisyen: 341 },
|
||||
"kutahya-dumlupinar-universitesi": { ogrenci: 38491, ogretimUyesi: 652, akademisyen: 1007 },
|
||||
"kutahya-saglik-bilimleri-universitesi": { ogrenci: 6757, ogretimUyesi: 326, akademisyen: 521 },
|
||||
"lokman-hekim-universitesi": { ogrenci: 5137, ogretimUyesi: 241, akademisyen: 331 },
|
||||
"malatya-turgut-ozal-universitesi": { ogrenci: 12370, ogretimUyesi: 355, akademisyen: 510 },
|
||||
"maltepe-universitesi": { ogrenci: 6288, ogretimUyesi: 370, akademisyen: 577 },
|
||||
"manisa-celal-bayar-universitesi": { ogrenci: 42218, ogretimUyesi: 986, akademisyen: 1647 },
|
||||
"mardin-artuklu-universitesi": { ogrenci: 16705, ogretimUyesi: 456, akademisyen: 722 },
|
||||
"marmara-universitesi": { ogrenci: 56733, ogretimUyesi: 2168, akademisyen: 3410 },
|
||||
"mef-universitesi": { ogrenci: 3708, ogretimUyesi: 115, akademisyen: 181 },
|
||||
"mersin-universitesi": { ogrenci: 37444, ogretimUyesi: 914, akademisyen: 1749 },
|
||||
"mimar-sinan-guzel-sanatlar-universitesi": { ogrenci: 8956, ogretimUyesi: 457, akademisyen: 631 },
|
||||
"mudanya-universitesi": { ogrenci: 3040, ogretimUyesi: 98, akademisyen: 158 },
|
||||
"mugla-sitki-kocman-universitesi": { ogrenci: 39185, ogretimUyesi: 991, akademisyen: 1727 },
|
||||
"munzur-universitesi": { ogrenci: 9826, ogretimUyesi: 318, akademisyen: 511 },
|
||||
"mus-alparslan-universitesi": { ogrenci: 14993, ogretimUyesi: 418, akademisyen: 647 },
|
||||
"necmettin-erbakan-universitesi": { ogrenci: 36239, ogretimUyesi: 1164, akademisyen: 1954 },
|
||||
"nevsehir-haci-bektas-veli-universitesi": { ogrenci: 21073, ogretimUyesi: 471, akademisyen: 822 },
|
||||
"nigde-omer-halisdemir-universitesi": { ogrenci: 26201, ogretimUyesi: 746, akademisyen: 1149 },
|
||||
"nuh-naci-yazgan-universitesi": { ogrenci: 2902, ogretimUyesi: 98, akademisyen: 162 },
|
||||
"ondokuz-mayis-universitesi": { ogrenci: 44852, ogretimUyesi: 1253, akademisyen: 2305 },
|
||||
"ordu-universitesi": { ogrenci: 17767, ogretimUyesi: 598, akademisyen: 973 },
|
||||
"orta-dogu-teknik-universitesi": { ogrenci: 30055, ogretimUyesi: 757, akademisyen: 2058 },
|
||||
"osmaniye-korkut-ata-universitesi": { ogrenci: 16460, ogretimUyesi: 365, akademisyen: 609 },
|
||||
"ostim-teknik-universitesi": { ogrenci: 8469, ogretimUyesi: 148, akademisyen: 257 },
|
||||
"ozyegin-universitesi": { ogrenci: 8369, ogretimUyesi: 255, akademisyen: 465 },
|
||||
"pamukkale-universitesi": { ogrenci: 43013, ogretimUyesi: 1238, akademisyen: 2113 },
|
||||
"piri-reis-universitesi": { ogrenci: 6741, ogretimUyesi: 121, akademisyen: 217 },
|
||||
"recep-tayyip-erdogan-universitesi": { ogrenci: 18693, ogretimUyesi: 703, akademisyen: 1252 },
|
||||
"sabanci-universitesi": { ogrenci: 5093, ogretimUyesi: 212, akademisyen: 363 },
|
||||
"saglik-bilimleri-universitesi": { ogrenci: 25070, ogretimUyesi: 3992, akademisyen: 4397 },
|
||||
"sakarya-universitesi": { ogrenci: 44027, ogretimUyesi: 1088, akademisyen: 1678 },
|
||||
"sakarya-uygulamali-bilimler-universitesi": { ogrenci: 29466, ogretimUyesi: 374, akademisyen: 698 },
|
||||
"samsun-universitesi": { ogrenci: 8287, ogretimUyesi: 363, akademisyen: 515 },
|
||||
"sanko-universitesi": { ogrenci: 1724, ogretimUyesi: 115, akademisyen: 167 },
|
||||
"selcuk-universitesi": { ogrenci: 61768, ogretimUyesi: 1486, akademisyen: 2594 },
|
||||
"siirt-universitesi": { ogrenci: 16165, ogretimUyesi: 482, akademisyen: 689 },
|
||||
"sinop-universitesi": { ogrenci: 13767, ogretimUyesi: 421, akademisyen: 626 },
|
||||
"sirnak-universitesi": { ogrenci: 6543, ogretimUyesi: 211, akademisyen: 330 },
|
||||
"sivas-bilim-ve-teknoloji-universitesi": { ogrenci: 940, ogretimUyesi: 81, akademisyen: 177 },
|
||||
"sivas-cumhuriyet-universitesi": { ogrenci: 46095, ogretimUyesi: 1184, akademisyen: 1921 },
|
||||
"suleyman-demirel-universitesi": { ogrenci: 33222, ogretimUyesi: 1097, akademisyen: 1847 },
|
||||
"tarsus-universitesi": { ogrenci: 7088, ogretimUyesi: 161, akademisyen: 285 },
|
||||
"ted-universitesi": { ogrenci: 5454, ogretimUyesi: 188, akademisyen: 331 },
|
||||
"tekirdag-namik-kemal-universitesi": { ogrenci: 28761, ogretimUyesi: 661, akademisyen: 1125 },
|
||||
"tobb-ekonomi-ve-teknoloji-universitesi": { ogrenci: 5597, ogretimUyesi: 242, akademisyen: 378 },
|
||||
"tokat-gaziosmanpasa-universitesi": { ogrenci: 32387, ogretimUyesi: 964, akademisyen: 1617 },
|
||||
"toros-universitesi": { ogrenci: 4319, ogretimUyesi: 96, akademisyen: 155 },
|
||||
"trabzon-universitesi": { ogrenci: 11965, ogretimUyesi: 432, akademisyen: 665 },
|
||||
"trakya-universitesi": { ogrenci: 35922, ogretimUyesi: 933, akademisyen: 1710 },
|
||||
"turk-alman-universitesi": { ogrenci: 4574, ogretimUyesi: 126, akademisyen: 364 },
|
||||
"turk-hava-kurumu-universitesi": { ogrenci: 4144, ogretimUyesi: 73, akademisyen: 162 },
|
||||
"ufuk-universitesi": { ogrenci: 4280, ogretimUyesi: 181, akademisyen: 284 },
|
||||
"usak-universitesi": { ogrenci: 22113, ogretimUyesi: 495, akademisyen: 930 },
|
||||
"uskudar-universitesi": { ogrenci: 23805, ogretimUyesi: 506, akademisyen: 702 },
|
||||
"van-yuzuncu-yil-universitesi": { ogrenci: 28497, ogretimUyesi: 1195, akademisyen: 1744 },
|
||||
"yalova-universitesi": { ogrenci: 16528, ogretimUyesi: 451, akademisyen: 758 },
|
||||
"yasar-universitesi": { ogrenci: 10081, ogretimUyesi: 226, akademisyen: 414 },
|
||||
"yeditepe-universitesi": { ogrenci: 19371, ogretimUyesi: 602, akademisyen: 1126 },
|
||||
"yildiz-teknik-universitesi": { ogrenci: 36618, ogretimUyesi: 1072, akademisyen: 1801 },
|
||||
"yozgat-bozok-universitesi": { ogrenci: 23118, ogretimUyesi: 656, akademisyen: 1028 },
|
||||
"yuksek-ihtisas-universitesi": { ogrenci: 3654, ogretimUyesi: 197, akademisyen: 272 },
|
||||
"zonguldak-bulent-ecevit-universitesi": { ogrenci: 29666, ogretimUyesi: 713, akademisyen: 1354 },
|
||||
"abdullah-gul-universitesi": { ogrenci: 3936, ogretimUyesi: 107, akademisyen: 300, kadin: 1931, erkek: 1761 },
|
||||
"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, kadin: 2405, erkek: 3131 },
|
||||
"adiyaman-universitesi": { ogrenci: 16659, ogretimUyesi: 548, akademisyen: 862, kadin: 7932, erkek: 8035 },
|
||||
"afyon-kocatepe-universitesi": { ogrenci: 29711, ogretimUyesi: 699, akademisyen: 1109, kadin: 13555, erkek: 13599 },
|
||||
"afyonkarahisar-saglik-bilimleri-universitesi": { ogrenci: 7072, ogretimUyesi: 325, akademisyen: 620, kadin: 4759, erkek: 2112 },
|
||||
"agri-ibrahim-cecen-universitesi": { ogrenci: 15239, ogretimUyesi: 390, akademisyen: 574, kadin: 8002, erkek: 6099 },
|
||||
"akdeniz-universitesi": { ogrenci: 55992, ogretimUyesi: 1339, akademisyen: 2658, kadin: 24232, erkek: 27483 },
|
||||
"aksaray-universitesi": { ogrenci: 18151, ogretimUyesi: 626, akademisyen: 963, kadin: 8731, erkek: 7227 },
|
||||
"alanya-alaaddin-keykubat-universitesi": { ogrenci: 15322, ogretimUyesi: 410, akademisyen: 696, kadin: 7155, erkek: 7297 },
|
||||
"alanya-universitesi": { ogrenci: 1207, ogretimUyesi: 51, akademisyen: 100, kadin: 606, erkek: 553 },
|
||||
"altinbas-universitesi": { ogrenci: 12055, ogretimUyesi: 227, akademisyen: 334, kadin: 5464, erkek: 5583 },
|
||||
"amasya-universitesi": { ogrenci: 16656, ogretimUyesi: 543, akademisyen: 748, kadin: 8278, erkek: 6878 },
|
||||
"anadolu-universitesi": { ogrenci: 25934, ogretimUyesi: 900, akademisyen: 1470, kadin: 12311, erkek: 9371 },
|
||||
"ankara-bilim-universitesi": { ogrenci: 3626, ogretimUyesi: 72, akademisyen: 134, kadin: 1813, erkek: 1689 },
|
||||
"ankara-haci-bayram-veli-universitesi": { ogrenci: 27559, ogretimUyesi: 867, akademisyen: 1221, kadin: 13583, erkek: 9130 },
|
||||
"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, kadin: 588, erkek: 613 },
|
||||
"ankara-sosyal-bilimler-universitesi": { ogrenci: 5879, ogretimUyesi: 274, akademisyen: 477, kadin: 2747, erkek: 1734 },
|
||||
"ankara-universitesi": { ogrenci: 56128, ogretimUyesi: 2225, akademisyen: 3702, kadin: 26197, erkek: 21099 },
|
||||
"ankara-yildirim-beyazit-universitesi": { ogrenci: 26177, ogretimUyesi: 1019, akademisyen: 1568, kadin: 13367, erkek: 8801 },
|
||||
"antalya-belek-universitesi": { ogrenci: 2553, ogretimUyesi: 44, akademisyen: 94, kadin: 1220, erkek: 1316 },
|
||||
"antalya-bilim-universitesi": { ogrenci: 8597, ogretimUyesi: 180, akademisyen: 315, kadin: 4696, erkek: 3440 },
|
||||
"ardahan-universitesi": { ogrenci: 7355, ogretimUyesi: 236, akademisyen: 380, kadin: 3751, erkek: 3092 },
|
||||
"artvin-coruh-universitesi": { ogrenci: 12358, ogretimUyesi: 332, akademisyen: 566, kadin: 6945, erkek: 4732 },
|
||||
"atasehir-adiguzel-meslek-yuksekokulu": { ogrenci: 1476, ogretimUyesi: 2, akademisyen: 32, kadin: 501, erkek: 975 },
|
||||
"ataturk-universitesi": { ogrenci: 58279, ogretimUyesi: 1800, akademisyen: 2758, kadin: 25889, erkek: 23030 },
|
||||
"atilim-universitesi": { ogrenci: 9885, ogretimUyesi: 331, akademisyen: 594, kadin: 4548, erkek: 4688 },
|
||||
"avrasya-universitesi": { ogrenci: 7495, ogretimUyesi: 119, akademisyen: 238, kadin: 4251, erkek: 3049 },
|
||||
"aydin-adnan-menderes-universitesi": { ogrenci: 43686, ogretimUyesi: 1232, akademisyen: 1924, kadin: 22196, erkek: 18397 },
|
||||
"bahcesehir-universitesi": { ogrenci: 21181, ogretimUyesi: 560, akademisyen: 838, kadin: 9131, erkek: 9225 },
|
||||
"balikesir-universitesi": { ogrenci: 28673, ogretimUyesi: 890, akademisyen: 1313, kadin: 13598, erkek: 13151 },
|
||||
"bandirma-onyedi-eylul-universitesi": { ogrenci: 18949, ogretimUyesi: 456, akademisyen: 653, kadin: 9102, erkek: 8604 },
|
||||
"bartin-universitesi": { ogrenci: 15615, ogretimUyesi: 452, akademisyen: 723, kadin: 7726, erkek: 6440 },
|
||||
"baskent-universitesi": { ogrenci: 17784, ogretimUyesi: 976, akademisyen: 1920, kadin: 9278, erkek: 7051 },
|
||||
"batman-universitesi": { ogrenci: 14701, ogretimUyesi: 360, akademisyen: 584, kadin: 5596, erkek: 7057 },
|
||||
"bayburt-universitesi": { ogrenci: 13883, ogretimUyesi: 314, akademisyen: 499, kadin: 6505, erkek: 6064 },
|
||||
"beykoz-universitesi": { ogrenci: 5251, ogretimUyesi: 94, akademisyen: 154, kadin: 1729, erkek: 2385 },
|
||||
"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, kadin: 9538, erkek: 8931 },
|
||||
"bingol-universitesi": { ogrenci: 18558, ogretimUyesi: 494, akademisyen: 744, kadin: 8860, erkek: 8174 },
|
||||
"biruni-universitesi": { ogrenci: 15154, ogretimUyesi: 347, akademisyen: 449, kadin: 9944, erkek: 4464 },
|
||||
"bitlis-eren-universitesi": { ogrenci: 14635, ogretimUyesi: 342, akademisyen: 549, kadin: 6782, erkek: 6699 },
|
||||
"bogazici-universitesi": { ogrenci: 13839, ogretimUyesi: 503, akademisyen: 1031, kadin: 5200, erkek: 6645 },
|
||||
"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, kadin: 16767, erkek: 15004 },
|
||||
"bursa-teknik-universitesi": { ogrenci: 12346, ogretimUyesi: 350, akademisyen: 601, kadin: 4583, erkek: 4884 },
|
||||
"bursa-uludag-universitesi": { ogrenci: 58341, ogretimUyesi: 1368, akademisyen: 2331, kadin: 25706, erkek: 26522 },
|
||||
"cag-universitesi": { ogrenci: 3667, ogretimUyesi: 63, akademisyen: 127, kadin: 1989, erkek: 1435 },
|
||||
"canakkale-onsekiz-mart-universitesi": { ogrenci: 42341, ogretimUyesi: 1254, akademisyen: 2058, kadin: 21004, erkek: 17456 },
|
||||
"cankaya-universitesi": { ogrenci: 6401, ogretimUyesi: 148, akademisyen: 260, kadin: 2651, erkek: 3040 },
|
||||
"cankiri-karatekin-universitesi": { ogrenci: 17291, ogretimUyesi: 488, akademisyen: 740, kadin: 8324, erkek: 7207 },
|
||||
"cukurova-universitesi": { ogrenci: 44430, ogretimUyesi: 1187, akademisyen: 2116, kadin: 18383, erkek: 21623 },
|
||||
"demiroglu-bilim-universitesi": { ogrenci: 2633, ogretimUyesi: 179, akademisyen: 231, kadin: 1861, erkek: 693 },
|
||||
"dicle-universitesi": { ogrenci: 42368, ogretimUyesi: 1011, akademisyen: 1641, kadin: 18148, erkek: 21068 },
|
||||
"dogus-universitesi": { ogrenci: 18442, ogretimUyesi: 217, akademisyen: 459, kadin: 8055, erkek: 9643 },
|
||||
"dokuz-eylul-universitesi": { ogrenci: 56833, ogretimUyesi: 1849, akademisyen: 2951, kadin: 23970, erkek: 27166 },
|
||||
"duzce-universitesi": { ogrenci: 28561, ogretimUyesi: 765, akademisyen: 1230, kadin: 12819, erkek: 13616 },
|
||||
"ege-universitesi": { ogrenci: 49852, ogretimUyesi: 1744, akademisyen: 2983, kadin: 21438, erkek: 22507 },
|
||||
"erciyes-universitesi": { ogrenci: 42735, ogretimUyesi: 1237, akademisyen: 2195, kadin: 18111, erkek: 17968 },
|
||||
"erzincan-binali-yildirim-universitesi": { ogrenci: 23617, ogretimUyesi: 764, akademisyen: 1070, kadin: 11460, erkek: 10260 },
|
||||
"erzurum-teknik-universitesi": { ogrenci: 5876, ogretimUyesi: 268, akademisyen: 389, kadin: 2530, erkek: 2245 },
|
||||
"eskisehir-osmangazi-universitesi": { ogrenci: 26667, ogretimUyesi: 1043, akademisyen: 1695, kadin: 11573, erkek: 12061 },
|
||||
"eskisehir-teknik-universitesi": { ogrenci: 13840, ogretimUyesi: 467, akademisyen: 735, kadin: 4874, erkek: 7422 },
|
||||
"fatih-sultan-mehmet-vakif-universitesi": { ogrenci: 7676, ogretimUyesi: 169, akademisyen: 328, kadin: 3639, erkek: 2774 },
|
||||
"fenerbahce-universitesi": { ogrenci: 9741, ogretimUyesi: 195, akademisyen: 311, kadin: 5072, erkek: 3918 },
|
||||
"firat-universitesi": { ogrenci: 39181, ogretimUyesi: 1167, akademisyen: 1924, kadin: 16028, erkek: 18301 },
|
||||
"galatasaray-universitesi": { ogrenci: 4398, ogretimUyesi: 162, akademisyen: 310, kadin: 1655, erkek: 1521 },
|
||||
"gazi-universitesi": { ogrenci: 38023, ogretimUyesi: 1877, akademisyen: 3115, kadin: 15208, erkek: 14515 },
|
||||
"gaziantep-islam-bilim-ve-teknoloji-universitesi": { ogrenci: 5934, ogretimUyesi: 167, akademisyen: 248, kadin: 3341, erkek: 2249 },
|
||||
"gaziantep-universitesi": { ogrenci: 45986, ogretimUyesi: 906, akademisyen: 1689, kadin: 20656, erkek: 20524 },
|
||||
"gebze-teknik-universitesi": { ogrenci: 10152, ogretimUyesi: 354, akademisyen: 820, kadin: 3183, erkek: 4217 },
|
||||
"giresun-universitesi": { ogrenci: 24556, ogretimUyesi: 820, akademisyen: 1165, kadin: 12018, erkek: 10984 },
|
||||
"gumushane-universitesi": { ogrenci: 19558, ogretimUyesi: 425, akademisyen: 664, kadin: 9622, erkek: 8329 },
|
||||
"hacettepe-universitesi": { ogrenci: 45592, ogretimUyesi: 2015, akademisyen: 3536, kadin: 20858, erkek: 17305 },
|
||||
"hakkari-universitesi": { ogrenci: 5255, ogretimUyesi: 216, akademisyen: 332, kadin: 2459, erkek: 2485 },
|
||||
"halic-universitesi": { ogrenci: 15757, ogretimUyesi: 256, akademisyen: 470, kadin: 8201, erkek: 6671 },
|
||||
"harran-universitesi": { ogrenci: 28476, ogretimUyesi: 740, akademisyen: 1249, kadin: 13266, erkek: 13153 },
|
||||
"hasan-kalyoncu-universitesi": { ogrenci: 7697, ogretimUyesi: 206, akademisyen: 311, kadin: 4304, erkek: 2595 },
|
||||
"hatay-mustafa-kemal-universitesi": { ogrenci: 18471, ogretimUyesi: 685, akademisyen: 1146, kadin: 8223, erkek: 8149 },
|
||||
"hitit-universitesi": { ogrenci: 18433, ogretimUyesi: 492, akademisyen: 821, kadin: 8416, erkek: 8700 },
|
||||
"ibn-haldun-universitesi": { ogrenci: 1877, ogretimUyesi: 110, akademisyen: 171, kadin: 437, erkek: 337 },
|
||||
"igdir-universitesi": { ogrenci: 13947, ogretimUyesi: 314, akademisyen: 491, kadin: 6633, erkek: 5886 },
|
||||
"ihsan-dogramaci-bilkent-universitesi": { ogrenci: 12244, ogretimUyesi: 367, akademisyen: 707, kadin: 5445, erkek: 5633 },
|
||||
"inonu-universitesi": { ogrenci: 30854, ogretimUyesi: 948, akademisyen: 1584, kadin: 14947, erkek: 12717 },
|
||||
"isik-universitesi": { ogrenci: 5999, ogretimUyesi: 108, akademisyen: 189, kadin: 2740, erkek: 2994 },
|
||||
"iskenderun-teknik-universitesi": { ogrenci: 13168, ogretimUyesi: 263, akademisyen: 388, kadin: 4192, erkek: 8139 },
|
||||
"isparta-uygulamali-bilimler-universitesi": { ogrenci: 22373, ogretimUyesi: 415, akademisyen: 739, kadin: 9500, erkek: 11945 },
|
||||
"istanbul-29-mayis-universitesi": { ogrenci: 5492, ogretimUyesi: 129, akademisyen: 187, kadin: 3330, erkek: 1698 },
|
||||
"istanbul-arel-universitesi": { ogrenci: 14076, ogretimUyesi: 276, akademisyen: 519, kadin: 5603, erkek: 6012 },
|
||||
"istanbul-atlas-universitesi": { ogrenci: 10704, ogretimUyesi: 376, akademisyen: 501, kadin: 6128, erkek: 3872 },
|
||||
"istanbul-aydin-universitesi": { ogrenci: 47200, ogretimUyesi: 777, akademisyen: 1200, kadin: 22065, erkek: 22829 },
|
||||
"istanbul-beykent-universitesi": { ogrenci: 26693, ogretimUyesi: 501, akademisyen: 824, kadin: 12165, erkek: 12578 },
|
||||
"istanbul-bilgi-universitesi": { ogrenci: 19216, ogretimUyesi: 310, akademisyen: 568, kadin: 9301, erkek: 8697 },
|
||||
"istanbul-esenyurt-universitesi": { ogrenci: 14673, ogretimUyesi: 150, akademisyen: 320, kadin: 5961, erkek: 6142 },
|
||||
"istanbul-galata-universitesi": { ogrenci: 4213, ogretimUyesi: 125, akademisyen: 192, kadin: 2318, erkek: 1527 },
|
||||
"istanbul-gedik-universitesi": { ogrenci: 10243, ogretimUyesi: 175, akademisyen: 306, kadin: 3828, erkek: 4777 },
|
||||
"istanbul-gelisim-universitesi": { ogrenci: 36730, ogretimUyesi: 472, akademisyen: 775, kadin: 15462, erkek: 19072 },
|
||||
"istanbul-kent-universitesi": { ogrenci: 15262, ogretimUyesi: 231, akademisyen: 405, kadin: 8327, erkek: 5516 },
|
||||
"istanbul-kultur-universitesi": { ogrenci: 13322, ogretimUyesi: 268, akademisyen: 459, kadin: 5923, erkek: 6236 },
|
||||
"istanbul-medeniyet-universitesi": { ogrenci: 15978, ogretimUyesi: 746, akademisyen: 1141, kadin: 8362, erkek: 6266 },
|
||||
"istanbul-medipol-universitesi": { ogrenci: 49066, ogretimUyesi: 975, akademisyen: 1647, kadin: 29514, erkek: 17933 },
|
||||
"istanbul-nisantasi-universitesi": { ogrenci: 42789, ogretimUyesi: 485, akademisyen: 783, kadin: 16369, erkek: 24229 },
|
||||
"istanbul-okan-universitesi": { ogrenci: 17022, ogretimUyesi: 560, akademisyen: 823, kadin: 7519, erkek: 7264 },
|
||||
"istanbul-rumeli-universitesi": { ogrenci: 10118, ogretimUyesi: 165, akademisyen: 257, kadin: 4165, erkek: 5081 },
|
||||
"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, kadin: 2993, erkek: 2052 },
|
||||
"istanbul-sisli-meslek-yuksekokulu": { ogrenci: 10446, ogretimUyesi: 19, akademisyen: 80, kadin: 5364, erkek: 5082 },
|
||||
"istanbul-teknik-universitesi": { ogrenci: 33900, ogretimUyesi: 1253, akademisyen: 2524, kadin: 7933, erkek: 15180 },
|
||||
"istanbul-ticaret-universitesi": { ogrenci: 8384, ogretimUyesi: 167, akademisyen: 270, kadin: 3338, erkek: 3583 },
|
||||
"istanbul-topkapi-universitesi": { ogrenci: 24477, ogretimUyesi: 237, akademisyen: 424, kadin: 10362, erkek: 11967 },
|
||||
"istanbul-universitesi": { ogrenci: 52770, ogretimUyesi: 2125, akademisyen: 3379, kadin: 22750, erkek: 19201 },
|
||||
"istanbul-universitesi-cerrahpasa": { ogrenci: 29490, ogretimUyesi: 1318, akademisyen: 2053, kadin: 12244, erkek: 11973 },
|
||||
"istanbul-yeni-yuzyil-universitesi": { ogrenci: 20511, ogretimUyesi: 271, akademisyen: 424, kadin: 10571, erkek: 9173 },
|
||||
"istinye-universitesi": { ogrenci: 17697, ogretimUyesi: 525, akademisyen: 745, kadin: 9249, erkek: 6682 },
|
||||
"izmir-bakircay-universitesi": { ogrenci: 8576, ogretimUyesi: 340, akademisyen: 502, kadin: 4095, erkek: 2964 },
|
||||
"izmir-demokrasi-universitesi": { ogrenci: 11641, ogretimUyesi: 321, akademisyen: 450, kadin: 5995, erkek: 4944 },
|
||||
"izmir-ekonomi-universitesi": { ogrenci: 11623, ogretimUyesi: 356, akademisyen: 626, kadin: 6000, erkek: 5106 },
|
||||
"izmir-katip-celebi-universitesi": { ogrenci: 19148, ogretimUyesi: 778, akademisyen: 1215, kadin: 8539, erkek: 7813 },
|
||||
"izmir-konak-meslek-yuksekokulu": { ogrenci: 3816, ogretimUyesi: 28, akademisyen: 116, kadin: 2097, erkek: 1719 },
|
||||
"izmir-tinaztepe-universitesi": { ogrenci: 4685, ogretimUyesi: 199, akademisyen: 232, kadin: 3436, erkek: 1111 },
|
||||
"izmir-yuksek-teknoloji-enstitusu": { ogrenci: 7109, ogretimUyesi: 234, akademisyen: 685, kadin: 2696, erkek: 3170 },
|
||||
"kadir-has-universitesi": { ogrenci: 5237, ogretimUyesi: 163, akademisyen: 257, kadin: 2654, erkek: 1926 },
|
||||
"kafkas-universitesi": { ogrenci: 18892, ogretimUyesi: 712, akademisyen: 989, kadin: 9477, erkek: 7736 },
|
||||
"kahramanmaras-istiklal-universitesi": { ogrenci: 4080, ogretimUyesi: 104, akademisyen: 186, kadin: 1771, erkek: 2208 },
|
||||
"kahramanmaras-sutcu-imam-universitesi": { ogrenci: 30728, ogretimUyesi: 867, akademisyen: 1335, kadin: 14430, erkek: 14124 },
|
||||
"kapadokya-universitesi": { ogrenci: 10946, ogretimUyesi: 149, akademisyen: 276, kadin: 5715, erkek: 4525 },
|
||||
"karabuk-universitesi": { ogrenci: 37533, ogretimUyesi: 808, akademisyen: 1179, kadin: 13816, erkek: 19911 },
|
||||
"karadeniz-teknik-universitesi": { ogrenci: 28957, ogretimUyesi: 1198, akademisyen: 2144, kadin: 11736, erkek: 13659 },
|
||||
"karamanoglu-mehmetbey-universitesi": { ogrenci: 19098, ogretimUyesi: 641, akademisyen: 908, kadin: 9829, erkek: 7281 },
|
||||
"kastamonu-universitesi": { ogrenci: 25308, ogretimUyesi: 639, akademisyen: 980, kadin: 12535, erkek: 11192 },
|
||||
"kayseri-universitesi": { ogrenci: 14389, ogretimUyesi: 174, akademisyen: 315, kadin: 5906, erkek: 8025 },
|
||||
"kilis-7-aralik-universitesi": { ogrenci: 13370, ogretimUyesi: 304, akademisyen: 478, kadin: 7332, erkek: 5302 },
|
||||
"kirikkale-universitesi": { ogrenci: 29779, ogretimUyesi: 775, akademisyen: 1209, kadin: 13579, erkek: 13193 },
|
||||
"kirklareli-universitesi": { ogrenci: 22377, ogretimUyesi: 560, akademisyen: 915, kadin: 11206, erkek: 10284 },
|
||||
"kirsehir-ahi-evran-universitesi": { ogrenci: 20182, ogretimUyesi: 670, akademisyen: 990, kadin: 9812, erkek: 8915 },
|
||||
"koc-universitesi": { ogrenci: 8596, ogretimUyesi: 432, akademisyen: 838, kadin: 3308, erkek: 3834 },
|
||||
"kocaeli-saglik-ve-teknoloji-universitesi": { ogrenci: 6271, ogretimUyesi: 115, akademisyen: 152, kadin: 3400, erkek: 2611 },
|
||||
"kocaeli-universitesi": { ogrenci: 53036, ogretimUyesi: 1191, akademisyen: 2174, kadin: 21178, erkek: 24858 },
|
||||
"konya-gida-ve-tarim-universitesi": { ogrenci: 1614, ogretimUyesi: 36, akademisyen: 57, kadin: 699, erkek: 607 },
|
||||
"konya-teknik-universitesi": { ogrenci: 10688, ogretimUyesi: 312, akademisyen: 521, kadin: 2574, erkek: 5847 },
|
||||
"kto-karatay-universitesi": { ogrenci: 9768, ogretimUyesi: 211, akademisyen: 341, kadin: 5445, erkek: 3531 },
|
||||
"kutahya-dumlupinar-universitesi": { ogrenci: 38491, ogretimUyesi: 652, akademisyen: 1007, kadin: 16742, erkek: 18326 },
|
||||
"kutahya-saglik-bilimleri-universitesi": { ogrenci: 6757, ogretimUyesi: 326, akademisyen: 521, kadin: 4791, erkek: 1824 },
|
||||
"lokman-hekim-universitesi": { ogrenci: 5137, ogretimUyesi: 241, akademisyen: 331, kadin: 3509, erkek: 1412 },
|
||||
"malatya-turgut-ozal-universitesi": { ogrenci: 12370, ogretimUyesi: 355, akademisyen: 510, kadin: 5756, erkek: 6016 },
|
||||
"maltepe-universitesi": { ogrenci: 6288, ogretimUyesi: 370, akademisyen: 577, kadin: 2914, erkek: 2901 },
|
||||
"manisa-celal-bayar-universitesi": { ogrenci: 42218, ogretimUyesi: 986, akademisyen: 1647, kadin: 18597, erkek: 20366 },
|
||||
"mardin-artuklu-universitesi": { ogrenci: 16705, ogretimUyesi: 456, akademisyen: 722, kadin: 7591, erkek: 7108 },
|
||||
"marmara-universitesi": { ogrenci: 56733, ogretimUyesi: 2168, akademisyen: 3410, kadin: 24398, erkek: 22729 },
|
||||
"mef-universitesi": { ogrenci: 3708, ogretimUyesi: 115, akademisyen: 181, kadin: 1997, erkek: 1554 },
|
||||
"mersin-universitesi": { ogrenci: 37444, ogretimUyesi: 914, akademisyen: 1749, kadin: 17040, erkek: 17154 },
|
||||
"mimar-sinan-guzel-sanatlar-universitesi": { ogrenci: 8956, ogretimUyesi: 457, akademisyen: 631, kadin: 4901, erkek: 3136 },
|
||||
"mudanya-universitesi": { ogrenci: 3040, ogretimUyesi: 98, akademisyen: 158, kadin: 1859, erkek: 999 },
|
||||
"mugla-sitki-kocman-universitesi": { ogrenci: 39185, ogretimUyesi: 991, akademisyen: 1727, kadin: 18559, erkek: 18248 },
|
||||
"munzur-universitesi": { ogrenci: 9826, ogretimUyesi: 318, akademisyen: 511, kadin: 5264, erkek: 3562 },
|
||||
"mus-alparslan-universitesi": { ogrenci: 14993, ogretimUyesi: 418, akademisyen: 647, kadin: 7254, erkek: 6563 },
|
||||
"necmettin-erbakan-universitesi": { ogrenci: 36239, ogretimUyesi: 1164, akademisyen: 1954, kadin: 17173, erkek: 13281 },
|
||||
"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, kadin: 12434, erkek: 11500 },
|
||||
"nuh-naci-yazgan-universitesi": { ogrenci: 2902, ogretimUyesi: 98, akademisyen: 162, kadin: 1821, erkek: 952 },
|
||||
"ondokuz-mayis-universitesi": { ogrenci: 44852, ogretimUyesi: 1253, akademisyen: 2305, kadin: 21720, erkek: 18555 },
|
||||
"ordu-universitesi": { ogrenci: 17767, ogretimUyesi: 598, akademisyen: 973, kadin: 8453, erkek: 7543 },
|
||||
"orta-dogu-teknik-universitesi": { ogrenci: 30055, ogretimUyesi: 757, akademisyen: 2058, kadin: 8820, erkek: 13089 },
|
||||
"osmaniye-korkut-ata-universitesi": { ogrenci: 16460, ogretimUyesi: 365, akademisyen: 609, kadin: 7770, erkek: 7478 },
|
||||
"ostim-teknik-universitesi": { ogrenci: 8469, ogretimUyesi: 148, akademisyen: 257, kadin: 2235, erkek: 5740 },
|
||||
"ozyegin-universitesi": { ogrenci: 8369, ogretimUyesi: 255, akademisyen: 465, kadin: 3607, erkek: 4032 },
|
||||
"pamukkale-universitesi": { ogrenci: 43013, ogretimUyesi: 1238, akademisyen: 2113, kadin: 20835, erkek: 19216 },
|
||||
"piri-reis-universitesi": { ogrenci: 6741, ogretimUyesi: 121, akademisyen: 217, kadin: 1706, erkek: 4859 },
|
||||
"recep-tayyip-erdogan-universitesi": { ogrenci: 18693, ogretimUyesi: 703, akademisyen: 1252, kadin: 9135, erkek: 7622 },
|
||||
"sabanci-universitesi": { ogrenci: 5093, ogretimUyesi: 212, akademisyen: 363, kadin: 1586, erkek: 2540 },
|
||||
"saglik-bilimleri-universitesi": { ogrenci: 25070, ogretimUyesi: 3992, akademisyen: 4397, kadin: 15532, erkek: 8165 },
|
||||
"sakarya-universitesi": { ogrenci: 44027, ogretimUyesi: 1088, akademisyen: 1678, kadin: 19421, erkek: 16279 },
|
||||
"sakarya-uygulamali-bilimler-universitesi": { ogrenci: 29466, ogretimUyesi: 374, akademisyen: 698, kadin: 10819, erkek: 16785 },
|
||||
"samsun-universitesi": { ogrenci: 8287, ogretimUyesi: 363, akademisyen: 515, kadin: 3672, erkek: 3972 },
|
||||
"sanko-universitesi": { ogrenci: 1724, ogretimUyesi: 115, akademisyen: 167, kadin: 1138, erkek: 492 },
|
||||
"selcuk-universitesi": { ogrenci: 61768, ogretimUyesi: 1486, akademisyen: 2594, kadin: 29801, erkek: 25791 },
|
||||
"siirt-universitesi": { ogrenci: 16165, ogretimUyesi: 482, akademisyen: 689, kadin: 7554, erkek: 7429 },
|
||||
"sinop-universitesi": { ogrenci: 13767, ogretimUyesi: 421, akademisyen: 626, kadin: 6953, erkek: 5556 },
|
||||
"sirnak-universitesi": { ogrenci: 6543, ogretimUyesi: 211, akademisyen: 330, kadin: 2708, erkek: 2943 },
|
||||
"sivas-bilim-ve-teknoloji-universitesi": { ogrenci: 940, ogretimUyesi: 81, akademisyen: 177, kadin: 289, erkek: 465 },
|
||||
"sivas-cumhuriyet-universitesi": { ogrenci: 46095, ogretimUyesi: 1184, akademisyen: 1921, kadin: 22629, erkek: 19612 },
|
||||
"suleyman-demirel-universitesi": { ogrenci: 33222, ogretimUyesi: 1097, akademisyen: 1847, kadin: 17249, erkek: 13294 },
|
||||
"tarsus-universitesi": { ogrenci: 7088, ogretimUyesi: 161, akademisyen: 285, kadin: 3645, erkek: 2896 },
|
||||
"ted-universitesi": { ogrenci: 5454, ogretimUyesi: 188, akademisyen: 331, kadin: 3042, erkek: 2080 },
|
||||
"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, kadin: 2649, erkek: 2611 },
|
||||
"tokat-gaziosmanpasa-universitesi": { ogrenci: 32387, ogretimUyesi: 964, akademisyen: 1617, kadin: 15275, erkek: 13354 },
|
||||
"toros-universitesi": { ogrenci: 4319, ogretimUyesi: 96, akademisyen: 155, kadin: 2497, erkek: 1710 },
|
||||
"trabzon-universitesi": { ogrenci: 11965, ogretimUyesi: 432, akademisyen: 665, kadin: 6516, erkek: 4458 },
|
||||
"trakya-universitesi": { ogrenci: 35922, ogretimUyesi: 933, akademisyen: 1710, kadin: 17801, erkek: 16193 },
|
||||
"turk-alman-universitesi": { ogrenci: 4574, ogretimUyesi: 126, akademisyen: 364, kadin: 1812, erkek: 2405 },
|
||||
"turk-hava-kurumu-universitesi": { ogrenci: 4144, ogretimUyesi: 73, akademisyen: 162, kadin: 1525, erkek: 2420 },
|
||||
"ufuk-universitesi": { ogrenci: 4280, ogretimUyesi: 181, akademisyen: 284, kadin: 2298, erkek: 1645 },
|
||||
"usak-universitesi": { ogrenci: 22113, ogretimUyesi: 495, akademisyen: 930, kadin: 11786, erkek: 9920 },
|
||||
"uskudar-universitesi": { ogrenci: 23805, ogretimUyesi: 506, akademisyen: 702, kadin: 12647, erkek: 7975 },
|
||||
"van-yuzuncu-yil-universitesi": { ogrenci: 28497, ogretimUyesi: 1195, akademisyen: 1744, kadin: 12411, erkek: 12107 },
|
||||
"yalova-universitesi": { ogrenci: 16528, ogretimUyesi: 451, akademisyen: 758, kadin: 7958, erkek: 7132 },
|
||||
"yasar-universitesi": { ogrenci: 10081, ogretimUyesi: 226, akademisyen: 414, kadin: 4775, erkek: 4809 },
|
||||
"yeditepe-universitesi": { ogrenci: 19371, ogretimUyesi: 602, akademisyen: 1126, kadin: 9336, erkek: 8200 },
|
||||
"yildiz-teknik-universitesi": { ogrenci: 36618, ogretimUyesi: 1072, akademisyen: 1801, kadin: 11322, erkek: 14554 },
|
||||
"yozgat-bozok-universitesi": { ogrenci: 23118, ogretimUyesi: 656, akademisyen: 1028, kadin: 10902, erkek: 10839 },
|
||||
"yuksek-ihtisas-universitesi": { ogrenci: 3654, ogretimUyesi: 197, akademisyen: 272, kadin: 2466, erkek: 1148 },
|
||||
"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. */
|
||||
@@ -240,3 +245,14 @@ export function uniIstatistikBul(slug: string): UniIstatistik | null {
|
||||
export function ogrenciBasinaUye(i: UniIstatistik): number {
|
||||
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