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

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