Danışman araçları, TÜİK mezun istihdamı ve Jev çıktı denetimi

- Danışman: program_netleri, program_karsilastir ve bolum_istihdam araçları;
  program_ara'ya devlet/vakıf, öğretim türü ve sıralama parametreleri;
  program_detay'a trend cümlesi; bozuk araç argümanı modele hata döner.
- TÜİK Yükseköğretim İstihdam Göstergeleri 2025: bölüm bazında istihdam
  oranı, ilk işi bulma süresi ve kazanç grubu (src/lib/bolum-istihdam.ts,
  üretici scripts/tuik-istihdam-uret.py, kaynak Excel'ler data/kaynak/).
- Bölüm sayfasında "Mezun istihdamı" ayrıntı satırı ve alt sayfası;
  AkademikAyrinti başlık/ikon alır.
- Jev (TypeSafe AI) ile danışman cevabında garanti/olasılık dili denetimi;
  ihlalde cevabın sonuna düzeltme notu. Anahtar yoksa denetim atlanır.
- /gizlilik: TypeSafe AI (ABD) sağlayıcı satırı. KARARLAR.md: 5 Ekim kaydı.
- Üç katalog dosyasındaki bg-white → bg-card değişiklikleri de bu commit'te.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-05 00:15:05 +03:00
parent ab0b3a047f
commit 6564ec8948
19 changed files with 1189 additions and 25 deletions

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
Briefcase,
Building2,
Calculator,
ChevronRight,
@@ -32,7 +33,16 @@ import {
import { BolumProgramListesi } from "./bolum-program-listesi";
import { VeriKaynagi } from "./veri-kaynagi";
import { NavBaslik } from "./nav-baslik";
import { AkademikAyrinti } from "./akademik-ayrinti";
import { OzetKart } from "@/components/ozet-kart";
import {
BOLUM_ISTIHDAM_KAYNAK_URL,
BOLUM_ISTIHDAM_YIL,
ISTIHDAM_ORTALAMA,
bolumIstihdamBul,
type BolumIstihdam,
type IstihdamDuzeyi,
} from "@/lib/bolum-istihdam";
import { PagePixelDivider } from "@/components/pixel-decor";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { SiteFooter } from "@/components/site-footer";
@@ -64,6 +74,134 @@ function kartSayisi(n: number): { deger: string; birim?: string } {
return { deger: sayi(n) };
}
const ondalik = (n: number) =>
n.toLocaleString("tr-TR", { maximumFractionDigits: 1 });
/** Ayrıntı satırının ikinci satırı: içeride ne olduğunu söyler (DESIGN.md §4.9). */
function istihdamOnizleme(v: BolumIstihdam): string {
const parcalar = [
v.istihdam != null
? `Mezunların %${ondalik(v.istihdam)}'i kayıtlı çalışıyor`
: null,
v.isBulmaAy != null ? `ilk iş ${ondalik(v.isBulmaAy)} ayda` : null,
v.istihdam == null && v.kazanc
? `kazanç grubu: ${v.kazanc.toLocaleLowerCase("tr-TR")}`
: null,
].filter(Boolean);
const metin = parcalar.join(" · ");
return metin.charAt(0).toLocaleUpperCase("tr-TR") + metin.slice(1);
}
/**
* Mezun istihdamı: TÜİK Yükseköğretim İstihdam Göstergeleri (ülke geneli,
* bölüm bazında). Sayfada değil, özet kartlarının altındaki "Mezun
* istihdamı ›" satırının açtığı alt sayfada durur (DESIGN.md §4.9–4.11):
* kart ve süs ayraç yok, gösterge kareleri masonry, altında tek not ve
* kaynak. Renk sempati katar, anlam taşımaz — risk dili değildir. data-*
* işaretleri açılış koreografisinin sözleşmesi (akademik-ayrinti.tsx).
*/
function MezunIstihdami({
veri,
duzey,
}: {
veri: BolumIstihdam;
duzey: IstihdamDuzeyi;
}) {
const ort = ISTIHDAM_ORTALAMA[duzey];
const kare =
"flex flex-col items-center justify-center rounded-3xl px-3 text-white";
const etiket = "text-sm font-bold text-white/90";
const devSayi =
"font-heading text-5xl font-bold leading-none tracking-tight";
const orta = "font-heading text-3xl font-bold leading-none";
const istihdamKaresi =
veri.istihdam != null ? (
<div data-kare="sol" className={`${kare} min-h-40 bg-orange-500 py-4`}>
<dt className={etiket}>Kayıtlı çalışan</dt>
<dd className={`mt-3 ${devSayi}`}>%{ondalik(veri.istihdam)}</dd>
{ort.istihdam != null ? (
<dd className="mt-2 text-base font-bold text-white/90">
Türkiye: %{ondalik(ort.istihdam)}
</dd>
) : null}
</div>
) : null;
const kazancKaresi = veri.kazanc ? (
<div data-kare="sol" className={`${kare} min-h-24 bg-indigo-500 py-3`}>
<dd className={orta}>{veri.kazanc}</dd>
<dt className={`mt-1.5 ${etiket}`}>Kazanç grubu</dt>
</div>
) : null;
const sureKaresi =
veri.isBulmaAy != null ? (
<div data-kare="sag-ust" className={`${kare} min-h-40 bg-primary py-4`}>
<dt className={etiket}>İlk işi bulma</dt>
<dd className="mt-3 flex items-baseline justify-center gap-1">
<span className={devSayi}>{ondalik(veri.isBulmaAy)}</span>
<span className="text-base font-bold text-white/80">ay</span>
</dd>
{ort.isBulmaAy != null ? (
<dd className="mt-2 text-base font-bold text-white/90">
Türkiye: {ondalik(ort.isBulmaAy)} ay
</dd>
) : null}
</div>
) : null;
const solVar = istihdamKaresi != null || kazancKaresi != null;
return (
<div className="flex flex-col items-center text-center">
<dl
data-sahne
className="relative flex w-full max-w-md items-start gap-3"
>
{solVar ? (
<div className="flex flex-1 flex-col gap-3">
{istihdamKaresi}
{kazancKaresi}
</div>
) : null}
{sureKaresi ? (
<div
className={`flex flex-1 flex-col gap-3 ${solVar ? "mt-12" : ""}`}
>
{sureKaresi}
</div>
) : null}
</dl>
<p
data-sonra
className="mt-4 max-w-md text-sm leading-relaxed text-slate-500"
>
Türkiye geneli, tüm üniversitelerin{" "}
{duzey === "lisans" ? "lisans" : "ön lisans"} mezunları birlikte;
üniversiteye göre değişir. Kayıtlı çalışan, sigortalı bir işte
çalışanların oranıdır. Kazanç grubu bölümler arası kıyastır, maaş
değildir.
</p>
<div
data-sonra
className="mt-5 w-full max-w-md border-t border-slate-100 pt-4 text-xs leading-5 text-slate-500"
>
<p>
TÜİK, Yükseköğretim İstihdam Göstergeleri {BOLUM_ISTIHDAM_YIL} ·{" "}
<a
href={BOLUM_ISTIHDAM_KAYNAK_URL}
target="_blank"
rel="noopener"
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
>
veriportali.tuik.gov.tr
</a>
</p>
</div>
</div>
);
}
/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */
const PUAN_TURU_HAP: Record<string, string> = {
SAYISAL: "Sayısal puan",
@@ -295,13 +433,18 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
}
}
const { stats } = bolum;
// TÜİK mezun istihdamı: lisansı olan bölümde lisans tablosu, yalnız ön
// lisansı olanda ön lisans tablosu. Tabloda yoksa satır hiç çizilmez.
const istihdamDuzeyi: IstihdamDuzeyi =
bolum.lisansProgramlari.length > 0 ? "lisans" : "onlisans";
const istihdam = bolumIstihdamBul(slug, istihdamDuzeyi);
// Sayfaya özgü, veriden türeyen özet cümle (GEO: yapay zekâ cevap motorları
// sayfa başına ilk metni saklar; 1011 katalog sayfasının aynı şablon cümleyle
// açılması hem alıntıyı hem Google'ın "ölçekli içerik" politikasını zorlar —
// docs/arastirma/2026-09-24-geo-seo). Rakamlar tablodakiyle aynı kaynaktan.
const veriOzeti = ilkSayfa ? bolumVeriOzeti(bolum, tumProgramlar) : null;
const hapSinifi =
"inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800";
"inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800";
return (
<>
@@ -460,6 +603,16 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
/>
</dl>
) : null}
{ilkSayfa && istihdam ? (
<AkademikAyrinti
baglam={bolum.ad}
baslik="Mezun istihdamı"
onizleme={istihdamOnizleme(istihdam)}
ikon={<Briefcase className="size-5 text-primary" aria-hidden />}
>
<MezunIstihdami veri={istihdam} duzey={istihdamDuzeyi} />
</AkademikAyrinti>
) : null}
<PagePixelDivider seed={83} className="mx-auto mt-12 lg:mx-0" />