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:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}`
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user