fix: denetim bulguları — çip adı sahte 'tercihlerin değişmiş', çekmece kartı, senkron riski

- tercihFarki eski 'İş garantisi' parametresini yeni ada eşleyerek kıyaslar;
  ad değişikliği 3 kredilik güncelleme çağrısı üretmesin
- çekmece kartı yalnız profil yokken sıra ister, butonu sıra kapısını açar
  (profilsiz ziyaretçi /listem giriş duvarına gönderilmiyordu → çıkmaz)
- sunucuylaBirlestir risksiz gelen kayıtların riskini doldurur
- /paket'teki fiyat kıyasına kaynak satırı

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 16:06:15 +03:00
parent 0400aa5d77
commit f7ed8f2f0b
5 changed files with 54 additions and 15 deletions

View File

@@ -52,6 +52,9 @@ export default function PaketPage({
Tercih danışmanları bu dönemde 1.000–3.000 TL alıyor. KolayTercih
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
</p>
<p className="mt-2 text-xs text-slate-500">
Danışman fiyat aralığı: Armut ortalama fiyatı, Eylül 2026.
</p>
</Parallax>
<Suspense fallback={<PaketSatinAlSkeleton />}>

View File

@@ -46,7 +46,11 @@ import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
import { bolumBazAdi, turkishSlugify } from "@/lib/slug";
import { olay } from "@/lib/analitik";
import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari";
import { sonucaGitIste } from "../hooks/use-tercih-profili";
import {
profilToplamaAc,
sonucaGitIste,
useTercihProfili,
} from "../hooks/use-tercih-profili";
import { useOturumDurumu } from "../liste-oturum-sinyali";
import { useGirisUrl } from "../liste-giris-url";
import {
@@ -92,8 +96,10 @@ export function ListeCekmecesi() {
const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100);
// Profilsiz + ile eklenenlerin riski henüz hesaplanmamıştır; alt kart
// olmayan bir analizi "hazır" diye duyurmasın.
const riskEksik = liste.some((t) => t.risk == null);
// olmayan bir analizi "hazır" diye duyurmasın. Profil varken null risk,
// programın taban sırası olmadığındandır — sıra istemek yanlış olur.
const profil = useTercihProfili();
const riskEksik = !profil && liste.some((t) => t.risk == null);
function programaEkleGit() {
cekmeceKapat();
@@ -278,16 +284,30 @@ export function ListeCekmecesi() {
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
</p>
</div>
<Button
asChild
size="lg"
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href="/listem" onClick={cekmeceKapat}>
Listemde aç
{riskEksik ? (
<Button
size="lg"
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
onClick={() => {
cekmeceKapat();
profilToplamaAc(null, true, "arti_toast");
}}
>
Sıralamamı gir
<ArrowRight data-icon="inline-end" />
</Link>
</Button>
</Button>
) : (
<Button
asChild
size="lg"
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href="/listem" onClick={cekmeceKapat}>
Listemde aç
<ArrowRight data-icon="inline-end" />
</Link>
</Button>
)}
</div>
</DrawerFooter>
</DrawerContent>

View File

@@ -152,6 +152,9 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX);
kaydet();
yayinla();
// Başka cihazda profilsiz eklenmiş kayıtlar risksiz gelir; profil lokalde
// zaten varsa PROFIL_DEGISTI_EVENT yayılmaz, riski burada doldur.
if (liste.some((t) => t.risk == null)) riskleriYenile();
return liste;
}

View File

@@ -14,6 +14,7 @@
import {
PUAN_TURU_ETIKET,
UNIVERSITE_TIPI_ETIKET,
oncelikNormalize,
type TercihProfili,
} from "@/features/sihirbaz/sihirbaz-sabitler";
import type { RaporParams } from "./types/rapor";
@@ -69,7 +70,9 @@ export function tercihFarki(
) {
farklar.push("universiteTipi");
}
if (kumeFarkli(params.oncelikler, s.oncelikler)) farklar.push("oncelikler");
if (kumeFarkli(params.oncelikler?.map(oncelikNormalize), s.oncelikler)) {
farklar.push("oncelikler");
}
return farklar;
}
@@ -107,7 +110,8 @@ export function farkSatirlari(
yeni: UNIVERSITE_TIPI_ETIKET[s.universiteTipi],
};
case "oncelikler":
return { ...ortak, eski: liste(params.oncelikler), yeni: liste(s.oncelikler) };
return { ...ortak, eski: liste(params.oncelikler?.map(oncelikNormalize)),
yeni: liste(s.oncelikler), };
}
});
}

View File

@@ -111,7 +111,7 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
p.oncelikler
// Eski kayıtlı profiller: çip adı "İş garantisi" idi (vaat diliyle
// çeliştiği için değişti); seçim kaybolmasın diye yeni ada eşlenir.
.map((x) => (x === "İş garantisi" ? "İş bulma kolaylığı" : x))
.map(oncelikNormalize)
.filter(
(x): x is string =>
typeof x === "string" &&
@@ -124,6 +124,15 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
return { kategoriler, iller, universiteTipi, oncelikler };
}
/**
* Eski çip adı "İş garantisi" → "İş bulma kolaylığı". Kayıtlı profil ve eski
* rapor parametreleri eski adı taşıyabilir; karşılaştırma/gösterim öncesi
* yeni ada eşlenir ki ad değişikliği "tercihlerin değişmiş" diye okunmasın.
*/
export function oncelikNormalize<T>(x: T): T | string {
return x === "İş garantisi" ? "İş bulma kolaylığı" : x;
}
/** localStorage'taki ortak sıra + sihirbaz profilini güvenle doğrular. */
export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
if (typeof girdi !== "object" || girdi === null) return null;