feat(liste): danışman profili havuzu süzer (önce profil gevşetilir) + KVKK metin düzeltmeleri

- db.ts OzellikSuzgeci (dil, yalnız örgün, yalnız burslu, ücret üst sınırı)
  program_ozellik alt sorgusuyla; tablo yoksa süzgeç yok.
- rapor-havuzu: profilSuzgeci (ev ili + km → il listesi; sihirbazda il
  seçiliyse km uygulanmaz); havuz 24'ün altına düşerse gevşetme sırası
  profil → il → kategori. rapor.ts prompt'a 'danışmana söylediği tercihler'
  ve profil gevşetme notu; rapor-actions profili okuyup geçirir.
- /meraklisina şeması (huni-semasi SEMA + aria-label + figcaption) ve 4.
  bölüm metni yeni adım ve gevşetme sırasıyla eşitlendi (AGENTS.md kuralı).
- /api/soru: profil kaydı öncesi 'kaydedeyim mi?' onayı (PROFIL_ONAY_SOR).
- Gizlilik metni kodla eşitlendi: OpenRouter→DeepSeek (Anthropic değil),
  profil satırı, amaç+hukuki sebep, saklama süreleri, md. 11 hakları,
  18 yaş altı notu; unvan/adres ve aktarım dayanağı TODO. Giriş kartı:
  koşullar 'kabul', gizlilik 'okudum'. Sohbet girişine kişisel bilgi uyarısı.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-25 01:23:14 +03:00
parent aa2051732d
commit 5a04db7e4b
11 changed files with 385 additions and 86 deletions

View File

@@ -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);

View File

@@ -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&rsquo;ndeki (İrlanda) sunucularda işlenir. Ödemeler,
Türkiye&rsquo;de yerleşik iyzico altyapısında gerçekleşir. Danışman
sohbetine yazdığın mesajlar, yanıt üretilebilmesi için
Anthropic&rsquo;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&rsquo;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&rsquo;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>&ldquo;kaydedeyim mi?&rdquo;</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
&ldquo;Kaydettiklerim&rdquo; ç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
&ldquo;Çerezler ve analitik&rdquo;). 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&rsquo;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&rsquo;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&rsquo;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 &ldquo;50-100 bin&rdquo; 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>

View File

@@ -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>
</>
);

View File

@@ -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>{" "}

View File

@@ -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
&ldquo;Kaydettiklerim&rdquo; ç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&apos;ün altına düşerse filtrelerin nasıl
gevşetildiği, yine de yetmezse listenin eldeki programlarla, sana
gösterir — havuz 24&apos;ü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 />

View File

@@ -663,6 +663,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>

View File

@@ -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

View File

@@ -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"}`

View File

@@ -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";

View File

@@ -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);

View File

@@ -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, {