From 14f9b622b02e936a8eb7658b3ef4c9e5a86aead5 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Sun, 9 Aug 2026 00:12:25 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20tad=C4=B1ml=C4=B1k=20havuzuna=20=C3=BCn?= =?UTF-8?q?iversite=20tipi=20boyutu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- drizzle/0001_steady_donald_blake.sql | 3 + drizzle/meta/0001_snapshot.json | 907 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + scripts/tadimlik-uret.ts | 156 +-- src/app/api/tadimlik/route.ts | 13 +- .../rapor/components/tadimlik-satiri.tsx | 50 +- src/lib/appdb/schema.ts | 12 +- src/lib/tadimlik-havuzu.ts | 13 +- 8 files changed, 1077 insertions(+), 84 deletions(-) create mode 100644 drizzle/0001_steady_donald_blake.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0001_steady_donald_blake.sql b/drizzle/0001_steady_donald_blake.sql new file mode 100644 index 0000000..887ea57 --- /dev/null +++ b/drizzle/0001_steady_donald_blake.sql @@ -0,0 +1,3 @@ +DROP INDEX `tadimlik_anahtar`;--> statement-breakpoint +ALTER TABLE `tadimlik_havuzu` ADD `tip` text DEFAULT 'genel' NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `tadimlik_anahtar` ON `tadimlik_havuzu` (`kova_slug`,`tur`,`kategori_slug`,`tip`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..858c07a --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,907 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "46e3e804-4222-4e4c-9151-dd71510cded5", + "prevId": "a802fded-01d5-4d17-8b6f-fca5dfc2920d", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_message_id": { + "name": "client_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chat_messages_client_message_id_unique": { + "name": "chat_messages_client_message_id_unique", + "columns": [ + "client_message_id" + ], + "isUnique": true + }, + "chat_user_created": { + "name": "chat_user_created", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "chat_messages_user_id_user_id_fk": { + "name": "chat_messages_user_id_user_id_fk", + "tableFrom": "chat_messages", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credit_ledger": { + "name": "credit_ledger", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delta": { + "name": "delta", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "ledger_reason_ref": { + "name": "ledger_reason_ref", + "columns": [ + "reason", + "ref_id" + ], + "isUnique": true + }, + "ledger_user": { + "name": "ledger_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "orders": { + "name": "orders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "amount_kurus": { + "name": "amount_kurus", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "iyzico_token": { + "name": "iyzico_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "iyzico_payment_id": { + "name": "iyzico_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "paid_at": { + "name": "paid_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "orders_user": { + "name": "orders_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "orders_user_id_user_id_fk": { + "name": "orders_user_id_user_id_fk", + "tableFrom": "orders", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "params": { + "name": "params", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revision_count": { + "name": "revision_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reports_user_id_unique": { + "name": "reports_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "reports_user_id_user_id_fk": { + "name": "reports_user_id_user_id_fk", + "tableFrom": "reports", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_lists": { + "name": "saved_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "items": { + "name": "items", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profil": { + "name": "profil", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "saved_lists_user_id_unique": { + "name": "saved_lists_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "saved_lists_user_id_user_id_fk": { + "name": "saved_lists_user_id_user_id_fk", + "tableFrom": "saved_lists", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tadimlik_havuzu": { + "name": "tadimlik_havuzu", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kova_slug": { + "name": "kova_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tur": { + "name": "tur", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kategori_slug": { + "name": "kategori_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tip": { + "name": "tip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'genel'" + }, + "program_id": { + "name": "program_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dilim": { + "name": "dilim", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "gerekce": { + "name": "gerekce", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "risk_notu": { + "name": "risk_notu", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trend_ozeti": { + "name": "trend_ozeti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "program": { + "name": "program", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ornek_sira": { + "name": "ornek_sira", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "uretim_at": { + "name": "uretim_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tadimlik_anahtar": { + "name": "tadimlik_anahtar", + "columns": [ + "kova_slug", + "tur", + "kategori_slug", + "tip" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_paket": { + "name": "has_paket", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "kredi_bitti_at": { + "name": "kredi_bitti_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "kredi_hatirlatma_gonderildi_at": { + "name": "kredi_hatirlatma_gonderildi_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 465324e..0702c64 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1786053613118, "tag": "0000_baslangic", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1786223252523, + "tag": "0001_steady_donald_blake", + "breakpoints": true } ] } \ No newline at end of file diff --git a/scripts/tadimlik-uret.ts b/scripts/tadimlik-uret.ts index 72f5985..a869324 100644 --- a/scripts/tadimlik-uret.ts +++ b/scripts/tadimlik-uret.ts @@ -1,11 +1,13 @@ /** - * Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel") - * kombinasyonları için 1'er örnek tercih satırı (gerekce/riskNotu/trendOzeti) - * üretip app.db'deki tadimlik_havuzu tablosuna yazar. + * Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel") × + * (üniversite tipi: genel/devlet/vakıf) kombinasyonları için 1'er örnek + * tercih satırı (gerekce/riskNotu/trendOzeti) üretip app.db'deki + * tadimlik_havuzu tablosuna yazar. * * pnpm tadimlik # boş anahtarları doldurur (idempotent) * pnpm tadimlik --force # mevcut satırları da yeniden üretir * pnpm tadimlik --kova=10-50k --tur=say --kategori=muhendislik-teknoloji + * pnpm tadimlik --tip=vakif # yalnızca vakıf varyantları * pnpm tadimlik --mock # anahtar olsa da mock metin (dev/test) * pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms) * @@ -24,10 +26,16 @@ import { type DilimKey, type Program, type PuanTuruKey, + type UniturGrubu, } from "../src/types/yokatlas"; import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler"; import { havuzuKompaktJson, turLabel } from "../src/lib/rapor-havuzu"; -import { GENEL_KATEGORI, KOVALAR, kategoriSlugBul } from "../src/lib/tadimlik-havuzu"; +import { + GENEL_KATEGORI, + KOVALAR, + kategoriSlugBul, + type TadimlikTip, +} from "../src/lib/tadimlik-havuzu"; import { anahtarVar } from "../src/lib/ai/client"; import { yapilandirilmisUret } from "../src/lib/ai/cagri"; @@ -95,18 +103,22 @@ function programBilgisi(p: Program) { /** * Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden - * (boşsa "garanti") kategoriye uyan, 2025'te en çok yerleşen alan program. - * Tanınır/dolu programlar tadımlıkta daha ikna edici. + * (boşsa "garanti") kategoriye ve üniversite tipine uyan, 2025'te en çok + * yerleşen alan program. Tanınır/dolu programlar tadımlıkta daha ikna edici. */ function programSec( temsiliSira: number, tur: PuanTuruKey, kategori: string | null, + uniturGrubu?: UniturGrubu, ): { program: Program; dilim: DilimKey } | null { for (const dilim of ["dengeli", "garanti"] as const) { - const { programlar } = dilimAra(temsiliSira, tur, dilim, { limit: 60 }); + const { programlar } = dilimAra(temsiliSira, tur, dilim, { + limit: 60, + uniturGrubu, + }); const uygunlar = kategori - ? programlar.filter((p) => kategoriEslesir(p.isim, [kategori])) + ? programlar.filter((p) => kategoriEslesir(p.isim, [kategori], p.fakulte)) : programlar; if (uygunlar.length === 0) continue; const secilen = [...uygunlar].sort( @@ -152,6 +164,7 @@ async function main() { const kovaFiltre = bayrak("kova"); const turFiltre = bayrak("tur"); const kategoriFiltre = bayrak("kategori"); + const tipFiltre = bayrak("tip"); const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter( (t) => !turFiltre || t === turFiltre, @@ -161,6 +174,9 @@ async function main() { ...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })), ].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre); const kovalar = KOVALAR.filter((k) => !kovaFiltre || k.slug === kovaFiltre); + const tipler = (["genel", "devlet", "vakif"] as TadimlikTip[]).filter( + (t) => !tipFiltre || t === tipFiltre, + ); const mevcutlar = new Set( ( @@ -169,9 +185,10 @@ async function main() { kovaSlug: schema.tadimlikHavuzu.kovaSlug, tur: schema.tadimlikHavuzu.tur, kategoriSlug: schema.tadimlikHavuzu.kategoriSlug, + tip: schema.tadimlikHavuzu.tip, }) .from(schema.tadimlikHavuzu) - ).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`), + ).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}|${s.tip}`), ); if (mockAktif()) { @@ -185,55 +202,46 @@ async function main() { for (const kova of kovalar) { for (const tur of turlar) { for (const kategori of kategoriler) { - const anahtar = `${kova.slug}|${tur}|${kategori.slug}`; - if (!force && mevcutlar.has(anahtar)) { - atlananMevcut++; - continue; - } + for (const tip of tipler) { + const anahtar = `${kova.slug}|${tur}|${kategori.slug}|${tip}`; + if (!force && mevcutlar.has(anahtar)) { + atlananMevcut++; + continue; + } - const secim = programSec(kova.temsiliSira, tur, kategori.ad); - if (!secim) { - atlananBos++; - continue; - } - - const metin = await metinUret( - secim.program, - secim.dilim, - kova.temsiliSira, - tur, - kategori.ad, - ); - if (!metin) { - console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`); - atlananBos++; - continue; - } - - const now = new Date(); - await appDb - .insert(schema.tadimlikHavuzu) - .values({ - id: randomUUID(), - kovaSlug: kova.slug, + const secim = programSec( + kova.temsiliSira, tur, - kategoriSlug: kategori.slug, - programId: secim.program.id, - dilim: secim.dilim, - gerekce: metin.gerekce, - riskNotu: metin.riskNotu, - trendOzeti: metin.trendOzeti, - program: programBilgisi(secim.program), - ornekSira: kova.temsiliSira, - uretimAt: now, - }) - .onConflictDoUpdate({ - target: [ - schema.tadimlikHavuzu.kovaSlug, - schema.tadimlikHavuzu.tur, - schema.tadimlikHavuzu.kategoriSlug, - ], - set: { + kategori.ad, + tip === "genel" ? undefined : tip, + ); + if (!secim) { + atlananBos++; + continue; + } + + const metin = await metinUret( + secim.program, + secim.dilim, + kova.temsiliSira, + tur, + kategori.ad, + ); + if (!metin) { + console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`); + atlananBos++; + continue; + } + + const now = new Date(); + await appDb + .insert(schema.tadimlikHavuzu) + .values({ + id: randomUUID(), + kovaSlug: kova.slug, + tur, + kategoriSlug: kategori.slug, + tip, programId: secim.program.id, dilim: secim.dilim, gerekce: metin.gerekce, @@ -242,15 +250,35 @@ async function main() { program: programBilgisi(secim.program), ornekSira: kova.temsiliSira, uretimAt: now, - }, - }); + }) + .onConflictDoUpdate({ + target: [ + schema.tadimlikHavuzu.kovaSlug, + schema.tadimlikHavuzu.tur, + schema.tadimlikHavuzu.kategoriSlug, + schema.tadimlikHavuzu.tip, + ], + set: { + programId: secim.program.id, + dilim: secim.dilim, + gerekce: metin.gerekce, + riskNotu: metin.riskNotu, + trendOzeti: metin.trendOzeti, + program: programBilgisi(secim.program), + ornekSira: kova.temsiliSira, + uretimAt: now, + }, + }); - uretilen++; - console.log(`✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`); - // Gerçek LLM'de rate limit dostu ardışık tempo - if (!mockAktif()) { - const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10); - await new Promise((r) => setTimeout(r, bekleme)); + uretilen++; + console.log( + `✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`, + ); + // Gerçek LLM'de rate limit dostu ardışık tempo + if (!mockAktif()) { + const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10); + await new Promise((r) => setTimeout(r, bekleme)); + } } } } diff --git a/src/app/api/tadimlik/route.ts b/src/app/api/tadimlik/route.ts index 7ac98bf..d3ffa1b 100644 --- a/src/app/api/tadimlik/route.ts +++ b/src/app/api/tadimlik/route.ts @@ -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. diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 7c86e30..ea3d83a 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -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(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(() => {}); }; diff --git a/src/lib/appdb/schema.ts b/src/lib/appdb/schema.ts index 6ca8a7f..7614555 100644 --- a/src/lib/appdb/schema.ts +++ b/src/lib/appdb/schema.ts @@ -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), ], ); diff --git a/src/lib/tadimlik-havuzu.ts b/src/lib/tadimlik-havuzu.ts index 28e991a..62dd85b 100644 --- a/src/lib/tadimlik-havuzu.ts +++ b/src/lib/tadimlik-havuzu.ts @@ -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 { 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) {