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 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. aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
</p> </p>
<p className="mt-2 text-xs text-slate-500">
Danışman fiyat aralığı: Armut ortalama fiyatı, Eylül 2026.
</p>
</Parallax> </Parallax>
<Suspense fallback={<PaketSatinAlSkeleton />}> <Suspense fallback={<PaketSatinAlSkeleton />}>

View File

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

View File

@@ -152,6 +152,9 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX); liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX);
kaydet(); kaydet();
yayinla(); 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; return liste;
} }

View File

@@ -14,6 +14,7 @@
import { import {
PUAN_TURU_ETIKET, PUAN_TURU_ETIKET,
UNIVERSITE_TIPI_ETIKET, UNIVERSITE_TIPI_ETIKET,
oncelikNormalize,
type TercihProfili, type TercihProfili,
} from "@/features/sihirbaz/sihirbaz-sabitler"; } from "@/features/sihirbaz/sihirbaz-sabitler";
import type { RaporParams } from "./types/rapor"; import type { RaporParams } from "./types/rapor";
@@ -69,7 +70,9 @@ export function tercihFarki(
) { ) {
farklar.push("universiteTipi"); 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; return farklar;
} }
@@ -107,7 +110,8 @@ export function farkSatirlari(
yeni: UNIVERSITE_TIPI_ETIKET[s.universiteTipi], yeni: UNIVERSITE_TIPI_ETIKET[s.universiteTipi],
}; };
case "oncelikler": 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 p.oncelikler
// Eski kayıtlı profiller: çip adı "İş garantisi" idi (vaat diliyle // 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. // ç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( .filter(
(x): x is string => (x): x is string =>
typeof x === "string" && typeof x === "string" &&
@@ -124,6 +124,15 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
return { kategoriler, iller, universiteTipi, oncelikler }; 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. */ /** localStorage'taki ortak sıra + sihirbaz profilini güvenle doğrular. */
export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null { export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
if (typeof girdi !== "object" || girdi === null) return null; if (typeof girdi !== "object" || girdi === null) return null;