feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle

- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası)
- iyzico ödeme entegrasyonu ve paket satın alma akışı
- kredi sistemi ve uygulama veritabanı şeması (drizzle)
- AI destekli rapor üretimi ve tercih sihirbazı
- rapor yazdırma sayfası ve site header/kullanıcı menüsü

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bilalgursen
2026-07-22 02:13:39 +03:00
parent 7c3f17b9b5
commit cee6200237
64 changed files with 7966 additions and 89 deletions

179
src/lib/ai/rapor.ts Normal file
View File

@@ -0,0 +1,179 @@
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { getAnthropic, RAPOR_MODEL } from "./client";
import {
havuzOlustur,
havuzuKompaktJson,
turLabel,
type RaporParams,
type AdayProgram,
} from "../rapor-havuzu";
import { secimOzeti } from "../sihirbaz";
const TercihSchema = z.object({
programId: z.string(),
sira: z.number().int(),
dilim: z.enum(["hayal", "dengeli", "garanti"]),
gerekce: z.string(),
riskNotu: z.string(),
trendOzeti: z.string(),
});
const RaporSchema = z.object({
tercihler: z.array(TercihSchema),
genelDegerlendirme: z.string(),
});
export type RaporSonuc = z.infer<typeof RaporSchema> & {
// Render için havuzdan zenginleştirilmiş program bilgisi
programlar: Record<
string,
{ isim: string; universite: string; il: string | null; unitur: string | null }
>;
};
const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Görevin: sana verilen
GERÇEK YÖK Atlas aday havuzundan, adayın sıralamasına göre 24 tercihlik dengeli
bir liste kurmak.
KURALLAR (kesin):
- SADECE havuzdaki programId'leri kullan. Havuzda olmayan program ASLA önerme.
- Tam 24 tercih üret; sira alanı 1'den 24'e sıralı olsun.
- Liste yapısı: en üstte 4-6 hayal, ortada 12-14 dengeli, sonda 5-7 garanti.
Tercih sırası MUTLAKA iyi taban sıralamasından kötüye doğru gitmeli
(küçük taban sıralaması = daha zor bölüm üstte).
- gerekce: bu programın bu aday için neden mantıklı olduğu (1-2 cümle, Türkçe).
- riskNotu: yerleşme ihtimali değerlendirmesi. ASLA "garanti", "kesin",
"yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan.
- trendOzeti: son 4 yılın taban sıralaması hareketi (ör. "2022'den beri taban
sıralaması 45B'den 38B'ye indi, bölüm zorlaşıyor").
- Kontenjan artışı/azalışı ve yerleşen sayısını değerlendirmene kat.
- Adayın notlarındaki şehir/bölüm tercihlerine saygı göster ama dengeyi bozma.
- genelDegerlendirme: listenin genel stratejisi + adaya 2-3 pratik uyarı (Türkçe).`;
export class RaporUretimHatasi extends Error {}
export async function raporUret(
params: RaporParams,
revizyonNotu?: string,
oncekiListeOzeti?: string,
): Promise<RaporSonuc> {
const { havuz, gevsetildi } = havuzOlustur(params);
if (havuz.length < 24) {
throw new RaporUretimHatasi(
"Bu sıralama ve filtrelerle yeterli program bulunamadı. Daha geniş kategori veya il seçmeyi dene.",
);
}
const secimVar =
(params.kategoriler?.length ?? 0) > 0 ||
(params.iller?.length ?? 0) > 0 ||
(params.oncelikler?.length ?? 0) > 0 ||
(params.universiteTipi && params.universiteTipi !== "farketmez");
const gevsetmeNotu: string[] = [];
if (gevsetildi.includes("il")) {
gevsetmeNotu.push(
"Adayın seçtiği illerde yeterli program yoktu; havuza başka illerden de program eklendi. Seçili illere öncelik ver ama listeyi 24'e tamamlamak için diğerlerini de kullan.",
);
}
if (gevsetildi.includes("kategori")) {
gevsetmeNotu.push(
"Adayın seçtiği ilgi alanlarında yeterli program yoktu; havuza yakın alanlardan program eklendi. Seçili alanlara öncelik ver.",
);
}
const client = getAnthropic();
const havuzJson = havuzuKompaktJson(havuz);
const kullaniciMesaji = [
`Aday bilgisi: başarı sıralaması ${params.sira.toLocaleString("tr-TR")}, puan türü ${turLabel(params.tur)}.`,
secimVar ? `ADAYIN SİHİRBAZ SEÇİMLERİ:\n${secimOzeti({
kategoriler: params.kategoriler ?? [],
iller: params.iller ?? [],
universiteTipi: params.universiteTipi ?? "farketmez",
oncelikler: params.oncelikler ?? [],
})}\nSeçili kategori ve illere öncelik ver; havuzun elverdiği ölçüde bu seçimlere sadık kal.`
: null,
params.notlar ? `Adayın ek notları: ${params.notlar}` : null,
gevsetmeNotu.length > 0 ? gevsetmeNotu.join("\n") : null,
revizyonNotu
? `REVİZYON İSTEĞİ: Aday mevcut listede şu değişikliği istiyor: ${revizyonNotu}\nÖnceki listenin özeti: ${oncekiListeOzeti ?? "yok"}`
: null,
`ADAY HAVUZU (yalnızca buradan seç): ${havuzJson}`,
]
.filter(Boolean)
.join("\n\n");
const gecerliIdler = new Set(havuz.map((p) => p.id));
let sonuc: z.infer<typeof RaporSchema> | null = null;
let sonHata = "";
for (let deneme = 0; deneme < 2; deneme++) {
const response = await client.messages.parse({
model: RAPOR_MODEL,
max_tokens: 16000,
thinking: { type: "adaptive" },
system: SISTEM,
messages: [
{
role: "user",
content:
deneme === 0
? kullaniciMesaji
: `${kullaniciMesaji}\n\nÖNCEKİ DENEMENDE HATA VARDI: ${sonHata} Kurallara tam uy.`,
},
],
output_config: { format: zodOutputFormat(RaporSchema) },
});
const parsed = response.parsed_output;
if (!parsed) {
sonHata = ıktı şemaya uymadı.";
continue;
}
// Halüsinasyon koruması: her programId havuzda olmalı + tam 24 öğe
const bilinmeyen = parsed.tercihler.filter(
(t) => !gecerliIdler.has(t.programId),
);
if (parsed.tercihler.length !== 24 || bilinmeyen.length > 0) {
sonHata =
parsed.tercihler.length !== 24
? `Liste ${parsed.tercihler.length} öğe içeriyor, tam 24 olmalı.`
: `Havuzda olmayan programId kullanıldı: ${bilinmeyen
.map((t) => t.programId)
.slice(0, 3)
.join(", ")}.`;
continue;
}
sonuc = parsed;
break;
}
if (!sonuc) {
throw new RaporUretimHatasi(
"Rapor üretilemedi (doğrulama iki denemede de başarısız). Tekrar dener misin? Revizyon hakkın kullanılmadı.",
);
}
const programlar: RaporSonuc["programlar"] = {};
for (const p of havuz) {
programlar[p.id] = {
isim: p.isim,
universite: p.universite,
il: p.il,
unitur: p.unitur,
};
}
return { ...sonuc, programlar };
}
export function listeOzetiCikar(rapor: RaporSonuc): string {
return rapor.tercihler
.map(
(t) =>
`${t.sira}. [${t.dilim}] ${rapor.programlar[t.programId]?.isim ?? "?"}${rapor.programlar[t.programId]?.universite ?? "?"}`,
)
.join("\n");
}
export type { AdayProgram };