fix(liste): kayıtlı manuel liste tabanları yeni veri yılına tazelenir; yıl metinleri 2021–2026
All checks were successful
Deploy / deploy (push) Successful in 7m24s

Denetim bulgusu (2026-09-21): ManuelTercih.efektifSira ekleme anının
snapshot'ıydı; 2026 verisi yüklenince /listem 2025 tabanıyla renk verirken
/sonuc tablosu 2026 gösteriyordu ve hiçbir yol yenilemiyordu.

- db.ts: efektifSiralarGetir(ids) — güncel COALESCE(sira2026, sira2025).
- liste-actions: listeTabanlariniTazele (oturumsuz, ≤24 id); sunucu kaydı
  (listeSenkronla/listeyiKaydet) tazelenmiş yazılır.
- liste-store: tabanlariGuncelle — efektif sıra + profil varsa risk yenilenir,
  arşivlenmiş program eski değerini korur, değişiklik yoksa yayın yok.
- liste-senkron: girişli/girişsiz herkes için sekme oturumu başına bir kez
  (anahtar SON_YIL taşır).
- meraklisina-icerik: "son beş yılın" → VERI_YIL_SAYISI.
- rehber (4 yazı): katalog sayfalarını anlatan "2021–2025 / son 5 yıl"
  ifadeleri 2021–2026 / son yıllar; yazıların kendi örnek tabloları
  2021–2025 olarak etiketli kaldı.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 18:59:05 +03:00
parent d275e4d29c
commit 49683fa113
9 changed files with 157 additions and 32 deletions

View File

@@ -13,9 +13,19 @@
import { useEffect, useRef } from "react";
import { PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku, tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil";
import { listeOku, sunucuylaBirlestir, useManuelListe } from "../liste-store";
import { SON_YIL } from "@/lib/veri-yillari";
import {
listeOku,
sunucuylaBirlestir,
tabanlariGuncelle,
useManuelListe,
} from "../liste-store";
import { oturumDurumuBildir } from "../liste-oturum-sinyali";
import { listeSenkronla, listeyiKaydet } from "../liste-actions";
import {
listeSenkronla,
listeTabanlariniTazele,
listeyiKaydet,
} from "../liste-actions";
const DEBOUNCE_MS = 2000;
@@ -29,6 +39,33 @@ export function ListeSenkron({ userId }: { userId: string | null }) {
oturumDurumuBildir(Boolean(userId));
}, [userId]);
// Yeni veri yılı: kayıtlı satırların efektif sırası ekleme anının
// snapshot'ıdır. Girişli/girişsiz herkes için, sekme oturumu başına bir kez
// güncel sıralar çekilir ve risk yeniden hesaplanır — /listem ile /sonuc
// tablosu aynı yılın tabanını göstersin. Anahtar SON_YIL'i taşır: yıl
// değişince eski işaret geçersizleşir.
useEffect(() => {
const mevcut = listeOku();
if (mevcut.length === 0) return;
const anahtar = `kolaytercih.liste-taban.${SON_YIL}`;
try {
if (sessionStorage.getItem(anahtar)) return;
} catch {}
let iptal = false;
void (async () => {
const tabanlar = await listeTabanlariniTazele(mevcut.map((t) => t.id));
if (iptal) return;
tabanlariGuncelle(tabanlar);
try {
sessionStorage.setItem(anahtar, "1");
} catch {}
})();
return () => {
iptal = true;
};
// Yalnız mount'ta: liste sonradan değişse de ekleme anı zaten güncel veriyle olur
}, []);
// Giriş anı merge'i — sekme oturumu başına bir kez (idempotent olsa da
// her gezinmede sunucuya gitmesin diye sessionStorage ile tekilleştirilir).
useEffect(() => {

View File

@@ -13,6 +13,7 @@
import { eq } from "drizzle-orm";
import { getSession } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { efektifSiralarGetir } from "@/lib/db";
import { tercihProfiliDogrula, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
import {
MANUEL_LISTE_MAX,
@@ -24,11 +25,48 @@ export type ListeSenkronSonucu =
| { ok: true; items: ManuelTercih[]; profil: TercihProfili | null }
| { ok: false };
const PROGRAM_ID = /^\d{1,12}$/;
/**
* Kayıtların taşıdığı efektif sıra ekleme anının snapshot'ıdır; yeni
* yerleştirme yılı yüklenince eskir. Güncel değeri DB'den okuyup satıra yazar;
* güncel kılavuzda olmayan program (arşiv) eski değerini korur. Risk burada
* yenilenmez — aday sırasını istemci bilir (bkz. liste-store tabanlariGuncelle).
*/
function tabanlariTazele(items: ManuelTercih[]): ManuelTercih[] {
const guncel = efektifSiralarGetir(
items.map((t) => t.id).filter((id) => PROGRAM_ID.test(id)),
);
return items.map((t) =>
t.id in guncel ? { ...t, efektifSira: guncel[t.id] } : t,
);
}
/**
* İstemcinin (girişli ya da girişsiz) localStorage listesini yeni veri yılına
* tazelemesi için: id → güncel efektif sıra. Program verisi herkese açık, oturum
* gerekmez. Yanıt yalnız listedeki (en fazla 24) id'yi kapsar.
*/
export async function listeTabanlariniTazele(
ids: unknown,
): Promise<Record<string, number | null>> {
if (!Array.isArray(ids)) return {};
const temiz = [
...new Set(
ids.filter(
(id): id is string => typeof id === "string" && PROGRAM_ID.test(id),
),
),
].slice(0, MANUEL_LISTE_MAX);
return efektifSiralarGetir(temiz);
}
async function kaydiYaz(
userId: string,
items: ManuelTercih[],
itemsHam: ManuelTercih[],
profil: TercihProfili | null,
): Promise<void> {
const items = tabanlariTazele(itemsHam);
const now = new Date();
await appDb
.insert(schema.savedLists)
@@ -65,10 +103,12 @@ export async function listeSenkronla(girdi: {
const sunucuProfil = tercihProfiliDogrula(mevcut?.profil);
const clientIdler = new Set(clientItems.map((t) => t.id));
const items = [
...clientItems,
...sunucuItems.filter((t) => !clientIdler.has(t.id)),
].slice(0, MANUEL_LISTE_MAX);
const items = tabanlariTazele(
[
...clientItems,
...sunucuItems.filter((t) => !clientIdler.has(t.id)),
].slice(0, MANUEL_LISTE_MAX),
);
const profil = clientProfil ?? sunucuProfil;
await kaydiYaz(userId, items, profil);

View File

@@ -194,6 +194,31 @@ export function programdanManuelTercih(
};
}
/**
* Yeni veri yılı: sunucudan gelen güncel efektif sıralarla kayıtları tazeler,
* profil varsa riski yeniden hesaplar. Haritada olmayan id (arşivlenmiş
* program) dokunulmaz. Değişiklik yoksa referans korunur, yayın yapılmaz.
*/
export function tabanlariGuncelle(tabanlar: Record<string, number | null>) {
yukle();
const profil = tercihProfiliOku();
let degisti = false;
const yeni = liste.map((t) => {
if (!(t.id in tabanlar) || tabanlar[t.id] === t.efektifSira) return t;
degisti = true;
const efektifSira = tabanlar[t.id];
return {
...t,
efektifSira,
risk: profil ? riskHesapla(efektifSira, profil.sira) : t.risk,
};
});
if (!degisti) return;
liste = yeni;
kaydet();
yayinla();
}
/** Profil sırasına göre listedeki riskleri yeniden hesaplar (program silmez). */
export function riskleriYenile(profil?: TercihProfili | null) {
yukle();

View File

@@ -8,6 +8,7 @@ import {
SiralamaDemo,
} from "./meraklisina-demo";
import { HuniSemasi } from "./huni-semasi";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
/**
* Algoritmanın öğrenci diline çevrilmiş anlatımı. Teknik detay (SQL, çarpan
@@ -135,8 +136,8 @@ export function MeraklisinaIcerik() {
Tüm bu hesaplar <strong>geçen yılların</strong>{" "}
yerleştirme sonuçlarına dayanır. Kontenjanlar, tercih davranışları
ve puanlar her yıl değişir; geçen yıl &ldquo;güvenli&rdquo;
görünen bir bölüm bu yıl dolabilir. Bu yüzden satırlarda son beş
yılın taban seyrini de gösteririz — çizgi yukarı tırmanıyorsa
görünen bir bölüm bu yıl dolabilir. Bu yüzden satırlarda son{" "}
{VERI_YIL_SAYISI} yılın taban seyrini de gösteririz — çizgi yukarı tırmanıyorsa
bölüm zorlaşıyor demektir. KolayTercih karar <em>destek</em>{" "}
aracıdır; listenin son hâli ve sorumluluğu her zaman sana aittir.
</p>

View File

@@ -210,6 +210,27 @@ export function netlerGetir(programId: string): ProgramNetSatiri[] {
.all(programId) as ProgramNetSatiri[];
}
/**
* Kayıtlı manuel liste satırlarının güncel efektif sırası (son yıl, yoksa bir
* önceki). Kayıtlar efektif sırayı ekleme anının snapshot'ı olarak taşır; yeni
* yerleştirme yılı yüklenince bununla tazelenir. Güncel kılavuzda olmayan
* (arşivlenmiş) id'ler sonuçta yer almaz — çağıran taraf eski değeri korur.
*/
export function efektifSiralarGetir(
ids: readonly string[]
): Record<string, number | null> {
if (ids.length === 0) return {};
const satirlar = getDb()
.prepare(
`SELECT id, ${EFEKTIF_SIRA} AS efektif FROM programs
WHERE id IN (${ids.map(() => "?").join(",")})`
)
.all(...ids) as { id: string; efektif: number | null }[];
const sonuc: Record<string, number | null> = {};
for (const s of satirlar) sonuc[s.id] = s.efektif;
return sonuc;
}
// Sihirbaz bubble'ları için erişilebilir program penceresi: hayal için
// gerçekçi alt sınırdan (sira*0.5) itibaren AÇIK uçlu — garanti tarafı artık
// sınırsız olduğundan üst kesim yok. Böylece 500B sıradaki aday Tıp bubble'ı