feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s
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:
119
src/app/api/onizleme/route.ts
Normal file
119
src/app/api/onizleme/route.ts
Normal 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" } });
|
||||
}
|
||||
148
src/app/api/program-ayrinti/route.ts
Normal file
148
src/app/api/program-ayrinti/route.ts
Normal 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" },
|
||||
});
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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’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’lük tercih
|
||||
listeni üretmek için. Özel kontenjan durumu ÖSYM’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>“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.
|
||||
öğ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
|
||||
|
||||
Reference in New Issue
Block a user