Files
kolaytercih/scripts/refresh.ts
bilalgursen 87f265ec15 Build TercihAI MVP: landing page, YÖK Atlas data pipeline, rank-based results
- Add product vision/market research doc (VISION.md)
- Set up shadcn/ui (radix + nova preset) with blue/orange theme, Outfit + Work Sans fonts
- Landing page: hero with rank input, problem/steps sections, comparison table, FAQ
- Data pipeline: CSV archive ingest (2021-2024) + live YÖK Atlas API refresh (2025)
  into SQLite (npm run ingest / refresh); zeros normalized to NULL
- /sonuc page: hayal/dengeli/garanti buckets by COALESCE(sira2025, sira2024),
  score-type switcher, 2024→2025 trend indicators
- Add ui-ux-pro-max and shadcn agent skills, shadcn MCP config, launch.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 07:46:53 +03:00

207 lines
5.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Canlı YÖK Atlas API'sinden 2025 yerleştirme verisini çekip DB'yi tazeler.
*
* Endpoint: POST https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search
* (yokatlas-py'nin kullandığı resmî tercih-kılavuz JSON API'si)
*
* - Mevcut programlarda sira/puan/kontenjan/yerlesen 2025 kolonlarını günceller
* - DB'de olmayan (yeni açılan) programları ekler
* - Az yerleşeni olan programlarda API basariSirasi döndürmez → NULL kalır
*
* Kullanım: npx tsx scripts/refresh.ts
*/
import path from "node:path";
import Database from "better-sqlite3";
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
const PAGE_SIZE = 500;
const DELAY_MS = 300;
const TUR_MAP: Record<string, string> = {
SAY: "SAYISAL",
EA: "EŞİT AĞIRLIK",
SÖZ: "SÖZEL",
DİL: "DİL",
TYT: "TYT",
};
type ApiRecord = {
kilavuzKodu: number;
birimAdi: string;
universiteAdi: string;
universiteTuru: string | null;
ilAdi: string | null;
fymkAdi: string | null;
puanTuru: string;
ogrenimSuresi: number | null;
birimTuruId: number;
basariSirasi?: number | null;
minPuan?: number | null;
kontenjan?: number | null;
gkY?: number | null;
sgyY?: number | null;
dprmY?: number | null;
obkY?: number | null;
};
type ApiPage = {
content: ApiRecord[];
totalPages: number;
totalElements: number;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function fetchPage(page: number): Promise<ApiPage> {
const res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filters: {
puanTuru: null,
universiteId: [],
birimGrupId: [],
ilKodu: [],
birimTuruId: null,
universiteTuru: null,
bursOraniId: null,
ogrenimTuruId: null,
kilavuzKodu: null,
minBasariSirasi: null,
maxBasariSirasi: null,
},
page,
size: PAGE_SIZE,
sortBy: "kilavuzKodu",
direction: "ASC",
}),
});
if (!res.ok) {
throw new Error(`API ${res.status} (sayfa ${page})`);
}
return (await res.json()) as ApiPage;
}
function yerlesen(r: ApiRecord): number | null {
const parts = [r.gkY, r.sgyY, r.dprmY, r.obkY].filter(
(x): x is number => typeof x === "number"
);
return parts.length ? parts.reduce((a, b) => a + b, 0) : null;
}
function intOrNull(v: unknown): number | null {
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
return typeof n === "number" && Number.isFinite(n) && n > 0
? Math.round(n)
: null;
}
function realOrNull(v: unknown): number | null {
const n = typeof v === "string" ? Number.parseFloat(v) : (v as number);
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : null;
}
async function main() {
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
db.pragma("journal_mode = WAL");
const cols = db.prepare("PRAGMA table_info(programs)").all() as {
name: string;
}[];
const has2025 = cols.some((c) => c.name === "sira2025");
if (!has2025) {
db.exec(`
ALTER TABLE programs ADD COLUMN sira2025 INTEGER;
ALTER TABLE programs ADD COLUMN puan2025 REAL;
ALTER TABLE programs ADD COLUMN kontenjan2025 INTEGER;
ALTER TABLE programs ADD COLUMN yerlesen2025 INTEGER;
CREATE INDEX IF NOT EXISTS idx_programs_tur_sira25 ON programs (tur, sira2025);
`);
}
const update = db.prepare(`
UPDATE programs SET sira2025 = ?, puan2025 = ?, kontenjan2025 = ?, yerlesen2025 = ?
WHERE id = ?
`);
const insert = db.prepare(`
INSERT INTO programs (id, isim, universite, unitur, il, fakulte, tur, sure, onlisans,
sira2025, puan2025, kontenjan2025, yerlesen2025)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const exists = db.prepare("SELECT 1 FROM programs WHERE id = ?");
let updated = 0;
let inserted = 0;
let skipped = 0;
const first = await fetchPage(0);
const totalPages = first.totalPages;
console.log(
`Toplam ${first.totalElements} kayıt, ${totalPages} sayfa çekilecek…`
);
const processPage = db.transaction((records: ApiRecord[]) => {
for (const r of records) {
const tur = TUR_MAP[r.puanTuru?.trim() ?? ""];
if (!r.kilavuzKodu || !tur) {
skipped++;
continue;
}
const id = String(r.kilavuzKodu);
const vals = [
intOrNull(r.basariSirasi),
realOrNull(r.minPuan),
intOrNull(r.kontenjan),
yerlesen(r),
];
if (exists.get(id)) {
update.run(...vals, id);
updated++;
} else {
insert.run(
id,
r.birimAdi,
r.universiteAdi,
r.universiteTuru ?? null,
r.ilAdi ?? null,
r.fymkAdi ?? null,
tur,
r.ogrenimSuresi ?? null,
r.birimTuruId === 47 ? 1 : 0,
...vals
);
inserted++;
}
}
});
processPage(first.content);
for (let p = 1; p < totalPages; p++) {
await sleep(DELAY_MS);
const page = await fetchPage(p);
processPage(page.content);
process.stdout.write(`\rsayfa ${p + 1}/${totalPages}`);
}
console.log();
const stats = db
.prepare(
`SELECT COUNT(*) AS toplam,
SUM(CASE WHEN sira2025 IS NOT NULL THEN 1 ELSE 0 END) AS sirali
FROM programs`
)
.get() as { toplam: number; sirali: number };
console.log(
`Güncellenen: ${updated}, eklenen: ${inserted}, atlanan: ${skipped}`
);
console.log(
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
);
db.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});