feat: tadımlık havuzuna üniversite tipi boyutu
Tadımlık satırı batch üretildiği için sihirbazdaki devlet/vakıf seçimini bilmiyor, vakıf seçen kullanıcıya devlet örneği gösteriyordu. Havuz anahtarı kova×tür×kategori×tip'e genişledi (migration 0001; mevcut satırlar tip='genel'). Üretim scripti tip varyantlarını uniturGrubu filtresiyle seçer; tadimlikSec tip verildiğinde kategori→genel fallback'i korur ama tip fallback'i YAPMAZ — uygun satır yoksa bileşen gizlenir, yanlış tipte örnek asla gösterilmez. Client profildeki tipi de gönderir ve SSR "genel" satırı tipe uymuyorsa fetch bitene kadar gizler. Not: prod app.db volume'de — `pnpm tadimlik` prod DB'ye karşı ayrıca koşulmalı. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,18 @@ export async function GET(istek: Request) {
|
||||
? kategoriParam
|
||||
: undefined;
|
||||
|
||||
const tadimlik = await tadimlikSec(sira, tur as PuanTuruKey, kategoriSlug);
|
||||
// Sihirbazdaki devlet/vakıf tercihi: uygun tipte satır yoksa null döner,
|
||||
// yanlış tipte örnek asla servis edilmez.
|
||||
const tipParam = url.searchParams.get("tip");
|
||||
const tip =
|
||||
tipParam === "devlet" || tipParam === "vakif" ? tipParam : "genel";
|
||||
|
||||
const tadimlik = await tadimlikSec(
|
||||
sira,
|
||||
tur as PuanTuruKey,
|
||||
kategoriSlug,
|
||||
tip,
|
||||
);
|
||||
return NextResponse.json(
|
||||
{ tadimlik },
|
||||
// Satırlar kova granülaritesinde statik; 1 saat cache yeterli.
|
||||
|
||||
@@ -27,10 +27,17 @@ import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
||||
import { turkishSlugify } from "@/lib/slug";
|
||||
import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||
import type { TadimlikSatir } from "@/lib/tadimlik-havuzu";
|
||||
import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
|
||||
|
||||
const GENEL = "genel";
|
||||
|
||||
/** Program tipi ile sihirbaz tercihi eşleşiyor mu (uniturFiltreSql semantiği). */
|
||||
function tipeUyar(program: TadimlikSatir["program"], tip: TadimlikTip): boolean {
|
||||
if (tip === "genel") return true;
|
||||
if (tip === "devlet") return program.unitur === "DEVLET";
|
||||
return program.unitur != null && program.unitur !== "DEVLET";
|
||||
}
|
||||
|
||||
export function TadimlikSatiri({
|
||||
sira,
|
||||
tur,
|
||||
@@ -42,29 +49,46 @@ export function TadimlikSatiri({
|
||||
ilk: TadimlikSatir | null;
|
||||
}) {
|
||||
const [satir, setSatir] = useState<TadimlikSatir | null>(ilk);
|
||||
const yuklenenSlug = useRef(GENEL);
|
||||
const yuklenenAnahtar = useRef(`${GENEL}|${GENEL}`);
|
||||
|
||||
// Sihirbaz kategorisi yalnızca localStorage'da yaşar; mount'ta ve profil
|
||||
// değiştikçe okunur, kategoriye özel satır sunucudan çekilir.
|
||||
// Sihirbaz kategorisi ve devlet/vakıf tercihi yalnızca localStorage'da
|
||||
// yaşar; mount'ta ve profil değiştikçe okunur, seçime özel satır sunucudan
|
||||
// çekilir. Tip tercihi varsa yanlış tipte örnek bir an bile gösterilmez.
|
||||
useEffect(() => {
|
||||
const guncelle = () => {
|
||||
const profil = tercihProfiliOku();
|
||||
const kategori = profil?.secimler.kategoriler[0];
|
||||
const slug = kategori ? turkishSlugify(kategori) : GENEL;
|
||||
if (slug === yuklenenSlug.current) return;
|
||||
yuklenenSlug.current = slug;
|
||||
if (slug === GENEL) {
|
||||
const tipTercihi = profil?.secimler.universiteTipi;
|
||||
const tip: TadimlikTip =
|
||||
tipTercihi === "devlet" || tipTercihi === "vakif"
|
||||
? tipTercihi
|
||||
: GENEL;
|
||||
const anahtar = `${slug}|${tip}`;
|
||||
if (anahtar === yuklenenAnahtar.current) return;
|
||||
yuklenenAnahtar.current = anahtar;
|
||||
if (slug === GENEL && tip === GENEL) {
|
||||
setSatir(ilk);
|
||||
return;
|
||||
}
|
||||
fetch(
|
||||
`/api/tadimlik?sira=${sira}&tur=${encodeURIComponent(tur)}&kategori=${encodeURIComponent(slug)}`,
|
||||
)
|
||||
// SSR "genel" satırı seçilen tipe uymuyorsa fetch bitene kadar gizle
|
||||
setSatir((mevcut) =>
|
||||
mevcut && !tipeUyar(mevcut.program, tip) ? null : mevcut,
|
||||
);
|
||||
const q = new URLSearchParams({ sira: String(sira), tur });
|
||||
if (slug !== GENEL) q.set("kategori", slug);
|
||||
if (tip !== GENEL) q.set("tip", tip);
|
||||
fetch(`/api/tadimlik?${q}`)
|
||||
.then((yanit) => (yanit.ok ? yanit.json() : null))
|
||||
.then((veri: { tadimlik: TadimlikSatir | null } | null) => {
|
||||
// Yarış koruması: bu arada başka kategori yüklendiyse dokunma
|
||||
if (yuklenenSlug.current !== slug) return;
|
||||
if (veri?.tadimlik) setSatir(veri.tadimlik);
|
||||
// Yarış koruması: bu arada başka seçim yüklendiyse dokunma
|
||||
if (yuklenenAnahtar.current !== anahtar) return;
|
||||
if (veri?.tadimlik) {
|
||||
setSatir(veri.tadimlik);
|
||||
} else if (tip !== GENEL) {
|
||||
// Uygun tipte satır yok: yanlış tip göstermektense gizle
|
||||
setSatir(null);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
@@ -154,8 +154,9 @@ export const reports = sqliteTable("reports", {
|
||||
});
|
||||
|
||||
// Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları.
|
||||
// Anahtar: sıra kovası × puan türü × kategori ("genel" = kategorisiz fallback).
|
||||
// Anonim istek LLM'e asla gitmez; bu tablo salt-okunur servis edilir.
|
||||
// Anahtar: sıra kovası × puan türü × kategori × üniversite tipi
|
||||
// ("genel" = kategorisiz/tipsiz fallback). Anonim istek LLM'e asla gitmez;
|
||||
// bu tablo salt-okunur servis edilir.
|
||||
// NOT: Yeni tablo/kolon eklerken `pnpm db:generate` ile migration üretilmeli;
|
||||
// prod şeması açılışta yalnızca drizzle/ altındaki migration'larla güncellenir.
|
||||
export const tadimlikHavuzu = sqliteTable(
|
||||
@@ -165,6 +166,11 @@ export const tadimlikHavuzu = sqliteTable(
|
||||
kovaSlug: text("kova_slug").notNull(),
|
||||
tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(),
|
||||
kategoriSlug: text("kategori_slug").notNull(),
|
||||
// Sihirbazdaki devlet/vakıf tercihine uyan varyant; "genel" = tip
|
||||
// filtresi olmayan satır (tipsiz kullanıcı bunu görür).
|
||||
tip: text("tip", { enum: ["genel", "devlet", "vakif"] })
|
||||
.notNull()
|
||||
.default("genel"),
|
||||
programId: text("program_id").notNull(),
|
||||
dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(),
|
||||
gerekce: text("gerekce").notNull(),
|
||||
@@ -178,7 +184,7 @@ export const tadimlikHavuzu = sqliteTable(
|
||||
uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug),
|
||||
uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug, t.tip),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -46,15 +46,21 @@ export interface TadimlikSatir {
|
||||
program: RaporSonuc["programlar"][string];
|
||||
}
|
||||
|
||||
export type TadimlikTip = "genel" | "devlet" | "vakif";
|
||||
|
||||
/**
|
||||
* Kova × tür × kategori anahtarıyla tadımlık satırı seçer. Kategoriye özel
|
||||
* satır yoksa "genel"e düşer; o da yoksa null — çağıran taraf bileşeni hiç
|
||||
* render etmez (boş/generic tadımlık gösterilmez, PRD kuralı).
|
||||
* Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye
|
||||
* özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf
|
||||
* bileşeni hiç render etmez (boş/generic tadımlık gösterilmez, PRD kuralı).
|
||||
*
|
||||
* Tip fallback'i YOKTUR: kullanıcı devlet/vakıf seçtiyse yanlış tipte örnek
|
||||
* asla gösterilmez — uygun satır yoksa tadımlık gizlenir.
|
||||
*/
|
||||
export async function tadimlikSec(
|
||||
sira: number,
|
||||
tur: PuanTuruKey,
|
||||
kategoriSlug?: string,
|
||||
tip: TadimlikTip = "genel",
|
||||
): Promise<TadimlikSatir | null> {
|
||||
const kova = kovaBul(sira);
|
||||
const sluglar = [
|
||||
@@ -66,6 +72,7 @@ export async function tadimlikSec(
|
||||
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
|
||||
eq(schema.tadimlikHavuzu.tur, tur),
|
||||
eq(schema.tadimlikHavuzu.kategoriSlug, slug),
|
||||
eq(schema.tadimlikHavuzu.tip, tip),
|
||||
),
|
||||
});
|
||||
if (satir) {
|
||||
|
||||
Reference in New Issue
Block a user