feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s

- Updated the app.db file to reflect recent changes in the application state.
- Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates.
- Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
This commit is contained in:
bilalgursen
2026-10-06 17:17:49 +03:00
parent c0696e2cbc
commit a3cb058a3b
74 changed files with 10334 additions and 445 deletions

View File

@@ -0,0 +1,119 @@
import { NextResponse, type NextRequest } from "next/server";
import { getAllBolumler, getAllUniversiteler, getIlBySlug } from "@/lib/katalog";
import { getRehber } from "@/lib/rehber";
import { uniLogoYolu } from "@/lib/uni-logolar";
import { UNITUR_ETIKET } from "@/lib/unitur";
import { URAP_TOPLAM, urapSirasiBul } from "@/lib/urap-siralama";
/**
* Danışman sohbetindeki bağlantı hapının üzerine gelince açılan sayfa
* önizlemesinin verisi (features/rapor/components/sohbet-baglanti.tsx,
* DESIGN.md §4.20). Yalnız site içi, herkese açık sayfalar; kişisel veri yok.
* Yılda bir değişen katalog/rehber verisi olduğu için bir gün cache'lenir.
*/
export type SayfaOnizlemesi = {
tur: "rehber" | "universite" | "bolum" | "sehir" | "paket" | "listem";
yol: string;
baslik: string;
aciklama: string;
/** Üniversite logosu gibi hazır görsel; yoksa istemci türe göre ikon çizer */
gorsel: string | null;
/** Kısa künye parçaları ("URAP 18/198", "Eki 2026") */
meta: string[];
};
const YOL_DESENI = /^\/(rehber|universite|bolum|universiteler)\/([a-z0-9-]{1,120})$/;
const sayi = (n: number) => n.toLocaleString("tr-TR");
function tarihKisa(iso: string): string | null {
const t = new Date(iso);
if (Number.isNaN(t.getTime())) return null;
return t.toLocaleDateString("tr-TR", { month: "short", year: "numeric" });
}
function onizlemeUret(yol: string): SayfaOnizlemesi | null {
if (yol === "/paket") {
return {
tur: "paket",
yol,
baslik: "Tercih Dönemi Paketi",
aciklama: "24 tercihin tamamında kişisel gerekçe ve risk notu, danışman soruları, revizyon ve PDF.",
gorsel: null,
meta: ["Tek seferlik"],
};
}
if (yol === "/listem") {
return {
tur: "listem",
yol,
baslik: "Listem",
aciklama: "24'lük tercih listen, haritası ve danışman sohbeti tek ekranda.",
gorsel: null,
meta: [],
};
}
const m = YOL_DESENI.exec(yol);
if (!m) return null;
const [, bolum, slug] = m;
if (bolum === "rehber") {
const yazi = getRehber(slug);
if (!yazi) return null;
const tarih = tarihKisa(yazi.guncelleme ?? yazi.tarih);
return {
tur: "rehber",
yol,
baslik: yazi.baslik,
aciklama: yazi.aciklama,
gorsel: null,
meta: ["Rehber yazısı", ...(tarih ? [tarih] : [])],
};
}
if (bolum === "universite") {
const u = getAllUniversiteler().find((x) => x.slug === slug);
if (!u) return null;
const tur = u.unitur ? UNITUR_ETIKET[u.unitur]?.cip : null;
const urap = urapSirasiBul(slug);
return {
tur: "universite",
yol,
baslik: u.ad,
aciklama: [u.il, tur, `${sayi(u.programSayisi)} program`].filter(Boolean).join(" · "),
gorsel: uniLogoYolu(slug),
meta: urap ? [`URAP ${urap.sira}/${URAP_TOPLAM}`] : [],
};
}
if (bolum === "bolum") {
const b = getAllBolumler().find((x) => x.slug === slug);
if (!b) return null;
const duzey = [b.lisans ? "Lisans" : null, b.onlisans ? "Ön lisans" : null].filter(Boolean).join(" + ");
return {
tur: "bolum",
yol,
baslik: b.ad,
aciklama: [`${sayi(b.programSayisi)} program`, duzey].filter(Boolean).join(" · "),
gorsel: null,
meta: b.enIyiSira != null ? [`En iyi taban sırası ${sayi(b.enIyiSira)}`] : [],
};
}
const il = getIlBySlug(slug);
if (!il) return null;
return {
tur: "sehir",
yol,
baslik: `${il.ad} Üniversiteleri`,
aciklama: `${sayi(il.uniler.length)} üniversite · ${sayi(il.programSayisi)} program`,
gorsel: null,
meta: [],
};
}
export function GET(request: NextRequest) {
const yol = request.nextUrl.searchParams.get("yol") ?? "";
if (!yol.startsWith("/") || yol.length > 160) {
return NextResponse.json({ error: "Geçerli bir site içi adres gerekli." }, { status: 400 });
}
const veri = onizlemeUret(yol);
if (!veri) return NextResponse.json({ error: "Sayfa bulunamadı." }, { status: 404 });
return NextResponse.json(veri, { headers: { "Cache-Control": "public, max-age=86400" } });
}

View File

@@ -0,0 +1,148 @@
import { NextResponse, type NextRequest } from "next/server";
import {
kontenjanTurleriGetir,
netlerGetir,
programGecmisiGetir,
programGetir,
} from "@/lib/db";
import { kosulMetinleri, ogretimUyesiToplami, programOzellikGetir, type ProgramOzellik } from "@/lib/program-ozellik";
import {
BOLUM_ISTIHDAM,
BOLUM_ISTIHDAM_KAYNAK,
BOLUM_ISTIHDAM_YIL,
ISTIHDAM_ORTALAMA,
type BolumIstihdam,
type IstihdamDuzeyi,
} from "@/lib/bolum-istihdam";
import { uniSlugFromAd } from "@/lib/katalog";
import { bolumSayfaSlug } from "@/lib/slug";
import { URAP_DONEM, URAP_TOPLAM, urapSirasiBul } from "@/lib/urap-siralama";
import {
OGRENCI_BASINA_UYE_MEDYAN,
YOK_ISTATISTIK_DONEM,
ogrenciBasinaUye,
uniIstatistikBul,
} from "@/lib/yok-istatistik";
import type {
KontenjanTurleri,
ProgramGecmisSatiri,
ProgramNetSatiri,
} from "@/types/yokatlas";
/**
* Program satırındaki "ayrıntı" düğmesinin verisi (program-ayrinti.tsx):
* künye (özellikler), yıllara göre sıra/puan/kontenjan/yerleşen, kontenjan
* türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı
* ve üniversitenin URAP/YÖK İstatistik özeti. Hepsi yılda bir değişen açık
* veri; istemci tarafında gönül rahatlığıyla cache'lenir.
*/
export type ProgramAyrintisi = {
program: {
id: string;
isim: string;
universite: string;
il: string | null;
unitur: string | null;
fakulte: string | null;
sure: number | null;
efektifSira: number | null;
};
ozellik: ProgramOzellik | null;
/** prof + doç + dr. öğr. üyesi (sunucuda hesaplanır; çekmece db modülünü import etmez) */
ogretimUyesiToplam: number | null;
kosullar: { kod: string; metin: string }[];
gecmis: ProgramGecmisSatiri[];
kontenjanTurleri: KontenjanTurleri | null;
netler: ProgramNetSatiri[];
istihdam:
| (BolumIstihdam & {
duzey: IstihdamDuzeyi;
yil: number;
kaynak: string;
ortalama: { istihdam: number | null; isBulmaAy: number | null };
bolumSayfasi: string;
})
| null;
universite: {
slug: string;
urap: { sira: number; toplam: number; donem: string } | null;
istatistik: {
donem: string;
ogrenci: number;
ogretimUyesi: number;
akademisyen: number;
hocaBasinaOgrenci: number;
medyan: number;
} | null;
} | null;
};
export function GET(request: NextRequest) {
const id = request.nextUrl.searchParams.get("id") ?? "";
if (!/^\d{1,12}$/.test(id)) {
return NextResponse.json(
{ error: "Geçerli bir program kimliği gerekli." },
{ status: 400 },
);
}
const p = programGetir(id);
if (!p) {
return NextResponse.json({ error: "Program bulunamadı." }, { status: 404 });
}
const ozellik = programOzellikGetir(id);
const duzey: IstihdamDuzeyi = p.tur === "TYT" ? "onlisans" : "lisans";
const bolumSlug = bolumSayfaSlug(p.isim);
const istihdamVeri = BOLUM_ISTIHDAM[duzey][bolumSlug] ?? null;
const uniSlug = uniSlugFromAd(p.universite);
const urap = uniSlug ? urapSirasiBul(uniSlug) : null;
const istatistik = uniSlug ? uniIstatistikBul(uniSlug) : null;
const veri: ProgramAyrintisi = {
program: {
id: p.id,
isim: p.isim.trim(),
universite: p.universite,
il: p.il,
unitur: p.unitur,
fakulte: p.fakulte,
sure: p.sure,
efektifSira: p.efektifSira,
},
ozellik,
ogretimUyesiToplam: ozellik ? ogretimUyesiToplami(ozellik) : null,
kosullar: ozellik ? kosulMetinleri(ozellik.kosulKodlari) : [],
gecmis: programGecmisiGetir(id),
kontenjanTurleri: kontenjanTurleriGetir(id),
netler: netlerGetir(id),
istihdam: istihdamVeri
? {
...istihdamVeri,
duzey,
yil: BOLUM_ISTIHDAM_YIL,
kaynak: BOLUM_ISTIHDAM_KAYNAK,
ortalama: ISTIHDAM_ORTALAMA[duzey],
bolumSayfasi: `/bolum/${bolumSlug}`,
}
: null,
universite: uniSlug
? {
slug: uniSlug,
urap: urap ? { sira: urap.sira, toplam: URAP_TOPLAM, donem: URAP_DONEM } : null,
istatistik: istatistik
? {
donem: YOK_ISTATISTIK_DONEM,
ogrenci: istatistik.ogrenci,
ogretimUyesi: istatistik.ogretimUyesi,
akademisyen: istatistik.akademisyen,
hocaBasinaOgrenci: ogrenciBasinaUye(istatistik),
medyan: OGRENCI_BASINA_UYE_MEDYAN,
}
: null,
}
: null,
};
return NextResponse.json(veri, {
headers: { "Cache-Control": "public, max-age=86400" },
});
}

View File

@@ -142,10 +142,11 @@ export async function POST(request: NextRequest) {
// Paketsiz kullanıcının prompt'una yalnızca açık satırlar girer — aksi
// halde danışman kilitli listeyi sözle sızdırabilir.
const kilitli = !kullanici?.hasPaket;
const raporSonuc = rapor.result as RaporSonuc;
const tamOzet = listeOzetiCikar(
rapor.result as RaporSonuc,
raporSonuc,
(rapor.params as RaporParams).sira,
{ kimlik: true },
{ kimlik: true, gerekce: true },
);
const listeOzeti = sohbetListeOzeti(tamOzet, kilitli);
@@ -156,14 +157,23 @@ export async function POST(request: NextRequest) {
kilitli,
profil,
barajlar: barajTablosu(),
rapor: {
genelDegerlendirme: raporSonuc.genelDegerlendirme,
uyarilar: raporSonuc.uyarilar,
kapsam: raporSonuc.kapsam ? { toplam: raporSonuc.kapsam.toplam, uyarlandi: raporSonuc.kapsam.uyarlandi } : undefined,
},
});
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci) ve profil
// (profil_guncelle yazınca aynı tur içinde tazelenir).
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci), profil
// (profil_guncelle yazınca aynı tur içinde tazelenir) ve sihirbaz seçimleri
// (uyum puanı "öne çıkan" cümleleri ve özel kontenjan eşlemesi için).
const aracBaglami: AracBaglami = {
userId: session.user.id,
sira: raporParams.sira,
tur: raporParams.tur,
profil,
oncelikler: raporParams.oncelikler ?? [],
ozelDurum: raporParams.ozelDurum ?? [],
sihirbazIlVar: (raporParams.iller?.length ?? 0) > 0 || Boolean(raporParams.il),
};
// Dev'de API key yoksa danışman mock cevap stream'ler (prod'da asla)

View File

@@ -45,10 +45,11 @@ export default function GizlilikPage() {
<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.
Senden yalnız e-posta, başarı sıralaman, sihirbaz seçimlerin
(isteğe bağlı özel kontenjan durumun dahil), 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
@@ -91,8 +92,13 @@ export default function GizlilikPage() {
</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ı.
(ilgi alanı, il, devlet/vakıf, öncelikler ve isteğe bağlı özel
kontenjan durumun: okul birincisi, şehit-gazi yakını,
depremzede, 34 yaş üstü kadın kontenjanı) — 24&rsquo;lük tercih
listeni üretmek için. Özel kontenjan durumu ÖSYM&rsquo;nin
ayırdığı kontenjan hakkının beyanıdır; cinsiyet alanı değildir,
işaretlemek zorunda değilsin, listeyi daraltmaz. Dayanak:
sözleşmenin ifası.
</li>
<li>
<strong>Danışman sohbetine yazdığın mesajlar</strong> —
@@ -106,9 +112,11 @@ export default function GizlilikPage() {
<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.
öğretim kabulün, kampüs büyüklüğü tercihin, özel kontenjan
durumun. 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 (34 yaş üstü kadın kontenjanı
beyanı cinsiyet alanı değil, ÖSYM kontenjan hakkıdır).
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