diff --git a/src/app/api/soru/route.ts b/src/app/api/soru/route.ts
index 66ba2ac..ac805b7 100644
--- a/src/app/api/soru/route.ts
+++ b/src/app/api/soru/route.ts
@@ -21,6 +21,13 @@ 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;
const MAX_MESAJ_UZUNLUK = 2000;
function sistemPromptu(opts: {
@@ -63,9 +70,14 @@ KURALLAR:
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_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:
+ kabul mü, kampüs büyüklüğü) ${PROFIL_ONAY_SOR
+ ? `HEMEN KAYDETME: cevabının sonunda tek cümleyle sor: "Bunu profiline
+ kaydedeyim mi? (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);
diff --git a/src/app/gizlilik/page.tsx b/src/app/gizlilik/page.tsx
index bcbad35..462ad38 100644
--- a/src/app/gizlilik/page.tsx
+++ b/src/app/gizlilik/page.tsx
@@ -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 (
<>
@@ -22,7 +30,7 @@ export default function GizlilikPage() {
Gizlilik ve Kişisel Verilerin Korunması
- Son güncelleme: 2 Ağustos 2026
+ Son güncelleme: 25 Eylül 2026
- 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.
-
+
+
Kısa özet
+
+
+ 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.
+
+
+ Bunlar yalnız tercih listeni üretmek ve sorularını yanıtlamak
+ için kullanılır; satılmaz, reklam için kullanılmaz.
+
+
+ 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.
+
+
+ Kaydedilen tercihlerini listem sayfasında görür, tek tıkla
+ silersin. Hesabını sildirmek için yazman yeterli.
+
+
+ 18 yaşından küçüksen bu metni velinle birlikte oku.
+
+
+
-
- Ne için kullanıyoruz?
-
+
Veri sorumlusu
- 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.
-
-
-
-
- Hizmet sağlayıcılar ve yurt dışına aktarım
-
-
- 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.
-
-
-
-
- Saklama ve silme
-
-
- 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ı{" "}
iletişim sayfasındaki
{" "}
- adresten bize yazabilirsin; talepler 30 gün içinde
- sonuçlandırılır.
+ e-posta adresine iletebilirsin.
+
-
- Çerezler ve analitik
-
+
Hangi verileri, ne için, hangi dayanakla işliyoruz?
+
+
+ E-posta adresin — hesap oluşturmak, giriş
+ bağlantısı ve ödeme bilgilendirmesi göndermek için.
+ Dayanak: seninle kurulan hizmet sözleşmesinin kurulması ve
+ ifası.
+
+
+ YKS başarı sıralaman ve sihirbaz seçimlerin{" "}
+ (ilgi alanı, il, devlet/vakıf, öncelikler) — 24’lük tercih
+ listeni üretmek için. Dayanak: sözleşmenin ifası.
+
+
+ Danışman sohbetine yazdığın mesajlar —
+ 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.
+
+
+ Danışman profilin — sohbette söylediğin ve{" "}
+ “kaydedeyim mi?” 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ı.
+
+
+ Sipariş kayıtların (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.
+
+
+ Kullanım ölçümü — 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.
+
+
+
+ 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.
+
+
+
+
+
Hizmet sağlayıcılar ve yurt dışına aktarım
+
+ 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:
+
+
+
+ Resend (İrlanda) — giriş bağlantısı ve
+ bilgilendirme e-postaları; e-posta adresin bu amaçla Avrupa
+ Birliği’ndeki sunucularda işlenir.
+
+
+ iyzico (Türkiye) — ödeme.
+
+
+ Yapay zekâ sağlayıcısı — 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.
+
+
+
+ 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.
+
+
+
+
+
Saklama ve silme
+
+
+ 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.
+
+
+ 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.
+
+
+ Sipariş ve fatura kayıtları vergi mevzuatı gereği 10 yıl
+ saklanır.
+
+
+
+
+
+
Hakların
+
+ 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{" "}
+
+ iletişim sayfasındaki
+ {" "}
+ e-posta adresine yaz; en geç 30 gün içinde yanıtlarız.
+
+
+
+
+
18 yaşından küçüksen
+
+ Bu metni velinle birlikte oku. Paket satın alınacaksa ödemeyi
+ velin yapmalı. Verilerin hiçbir şekilde pazarlama için
+ kullanılmaz.
+
+
+
+
+
Çerezler ve analitik
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.
diff --git a/src/features/kullanici/components/giris-karti.tsx b/src/features/kullanici/components/giris-karti.tsx
index 7da1add..c1a1b49 100644
--- a/src/features/kullanici/components/giris-karti.tsx
+++ b/src/features/kullanici/components/giris-karti.tsx
@@ -77,15 +77,17 @@ export async function GirisKarti({
- Giriş yaparak verilerinin tercih önerileri üretmek amacıyla{" "}
-
- gizlilik metnindeki
- {" "}
- 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{" "}
kullanım koşullarını
{" "}
- kabul etmiş olursun. KolayTercih yerleşme garantisi vermez.
+ kabul etmiş ve verilerinin nasıl işlendiğini anlatan{" "}
+
+ gizlilik metnini
+ {" "}
+ okumuş olursun. KolayTercih yerleşme garantisi vermez.
>
);
diff --git a/src/features/pazarlama/components/huni-semasi.tsx b/src/features/pazarlama/components/huni-semasi.tsx
index 4286fbb..ee1f190 100644
--- a/src/features/pazarlama/components/huni-semasi.tsx
+++ b/src/features/pazarlama/components/huni-semasi.tsx
@@ -16,9 +16,10 @@ flowchart TD
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi 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 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 sayılar hem alanı hem ili hesaba katar")
- C --> H("Aday havuzu elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar kapanan ya da kodu değişip eski kaydı kalan programlar önerilmez")
+ C --> P("Danışmana söylediklerin öğ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 elde kalan gerçek programlar — yalnızca en son tercih kılavuzunda yer alanlar 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 önce il, sonra alan — listede açıkça belirtilir")
+ S -- "evet" --> G("Filtreler adım adım gevşetilir önce danışmana söylediklerin, sonra il, sonra alan — listede açıkça belirtilir")
G --> H
H --> U("Havuz yine de dar kalırsa 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 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 (
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{" "}
dışına asla çıkmadan
{" "}
diff --git a/src/features/pazarlama/components/meraklisina-icerik.tsx b/src/features/pazarlama/components/meraklisina-icerik.tsx
index 1d62768..947ccac 100644
--- a/src/features/pazarlama/components/meraklisina-icerik.tsx
+++ b/src/features/pazarlama/components/meraklisina-icerik.tsx
@@ -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 aday havuzu{" "}
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
verilir. Havuza yalnızca{" "}
en son tercih kılavuzunda yer alan{" "}
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.
diff --git a/src/features/rapor/components/sohbet-client.tsx b/src/features/rapor/components/sohbet-client.tsx
index 6002573..2da7a06 100644
--- a/src/features/rapor/components/sohbet-client.tsx
+++ b/src/features/rapor/components/sohbet-client.tsx
@@ -663,6 +663,11 @@ export function SohbetClient({
+ {/* KVKK: sohbete kişisel bilgi yazılmasın (hukuk-ve-rakipler.md C.5/22) */}
+
+ Ad, telefon, kimlik numarası ya da sağlık bilgisi yazma; danışmanın
+ bunlara ihtiyacı yok.
+
>
)}
diff --git a/src/features/rapor/rapor-actions.ts b/src/features/rapor/rapor-actions.ts
index cad49f7..ebc3185 100644
--- a/src/features/rapor/rapor-actions.ts
+++ b/src/features/rapor/rapor-actions.ts
@@ -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
diff --git a/src/lib/ai/rapor.ts b/src/lib/ai/rapor.ts
index 13b6adf..1b86e2d 100644
--- a/src/lib/ai/rapor.ts
+++ b/src/lib/ai/rapor.ts
@@ -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 {
- 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"}`
diff --git a/src/lib/danisman-profil.ts b/src/lib/danisman-profil.ts
index 13ecc18..3bd5b46 100644
--- a/src/lib/danisman-profil.ts
+++ b/src/lib/danisman-profil.ts
@@ -8,6 +8,11 @@
* 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";
diff --git a/src/lib/db.ts b/src/lib/db.ts
index 0d3b1be..f173092 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -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);
diff --git a/src/lib/rapor-havuzu.ts b/src/lib/rapor-havuzu.ts
index 7efe92f..296fc44 100644
--- a/src/lib/rapor-havuzu.ts
+++ b/src/lib/rapor-havuzu.ts
@@ -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, {