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>
This commit is contained in:
190
scripts/ingest.ts
Normal file
190
scripts/ingest.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* YÖK Atlas verisini SQLite'a yükler.
|
||||
*
|
||||
* Kaynak: yokatlas-dataset-2025 (github.com/MorphaxTheDeveloper/yokatlas-dataset-2025)
|
||||
* — YÖK Atlas'ın halka açık verisinin CSV dökümü. Canlı API'den tazeleme
|
||||
* (yokatlas-py'nin kullandığı JSON API) v2'de eklenecek.
|
||||
*
|
||||
* Kullanım: npx tsx scripts/ingest.ts <tum_bolumler.csv yolu>
|
||||
*/
|
||||
import { createReadStream, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const YEARS = [2021, 2022, 2023, 2024] as const;
|
||||
|
||||
const csvPath = process.argv[2];
|
||||
if (!csvPath || !existsSync(csvPath)) {
|
||||
console.error("Kullanım: npx tsx scripts/ingest.ts <tum_bolumler.csv yolu>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dbDir = path.join(process.cwd(), "data");
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
const dbPath = path.join(dbDir, "yokatlas.db");
|
||||
rmSync(dbPath, { force: true });
|
||||
|
||||
const db = new Database(dbPath);
|
||||
db.pragma("journal_mode = WAL");
|
||||
|
||||
const yearCols = YEARS.flatMap((y) => [
|
||||
`sira${y} INTEGER`,
|
||||
`puan${y} REAL`,
|
||||
`kontenjan${y} INTEGER`,
|
||||
`yerlesen${y} INTEGER`,
|
||||
]);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE programs (
|
||||
id TEXT PRIMARY KEY,
|
||||
isim TEXT NOT NULL,
|
||||
universite TEXT NOT NULL,
|
||||
unitur TEXT,
|
||||
il TEXT,
|
||||
fakulte TEXT,
|
||||
tur TEXT NOT NULL,
|
||||
sure INTEGER,
|
||||
onlisans INTEGER NOT NULL DEFAULT 0,
|
||||
${yearCols.join(",\n ")}
|
||||
);
|
||||
CREATE INDEX idx_programs_tur_sira ON programs (tur, sira2024);
|
||||
CREATE INDEX idx_programs_il ON programs (il);
|
||||
`);
|
||||
|
||||
// CSV satırlarını RFC-4180 tırnak kurallarıyla böler (alan içi virgül/tırnak destekli)
|
||||
function splitCsvLine(line: string): string[] {
|
||||
const out: string[] = [];
|
||||
let cur = "";
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
if (inQuotes) {
|
||||
if (ch === '"') {
|
||||
if (line[i + 1] === '"') {
|
||||
cur += '"';
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = false;
|
||||
}
|
||||
} else {
|
||||
cur += ch;
|
||||
}
|
||||
} else if (ch === '"') {
|
||||
inQuotes = true;
|
||||
} else if (ch === ",") {
|
||||
out.push(cur);
|
||||
cur = "";
|
||||
} else {
|
||||
cur += ch;
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function toInt(v: string | undefined): number | null {
|
||||
if (!v) return null;
|
||||
const n = Number.parseFloat(v);
|
||||
return Number.isFinite(n) ? Math.round(n) : null;
|
||||
}
|
||||
|
||||
function toReal(v: string | undefined): number | null {
|
||||
if (!v) return null;
|
||||
const n = Number.parseFloat(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
// Kaynak CSV'de 0, "veri yok" anlamına gelir (sıralama/puan 0 olamaz)
|
||||
function positiveOrNull(n: number | null): number | null {
|
||||
return n != null && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const columns = [
|
||||
"id",
|
||||
"isim",
|
||||
"universite",
|
||||
"unitur",
|
||||
"il",
|
||||
"fakulte",
|
||||
"tur",
|
||||
"sure",
|
||||
"onlisans",
|
||||
...YEARS.flatMap((y) => [
|
||||
`sira${y}`,
|
||||
`puan${y}`,
|
||||
`kontenjan${y}`,
|
||||
`yerlesen${y}`,
|
||||
]),
|
||||
];
|
||||
const insert = db.prepare(
|
||||
`INSERT OR REPLACE INTO programs (${columns.join(",")})
|
||||
VALUES (${columns.map(() => "?").join(",")})`
|
||||
);
|
||||
|
||||
const rl = createInterface({
|
||||
input: createReadStream(csvPath, "utf8"),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let header: string[] | null = null;
|
||||
let idx: Record<string, number> = {};
|
||||
const rows: unknown[][] = [];
|
||||
let skipped = 0;
|
||||
|
||||
for await (const line of rl) {
|
||||
if (!header) {
|
||||
header = splitCsvLine(line);
|
||||
idx = Object.fromEntries(header.map((h, i) => [h, i]));
|
||||
continue;
|
||||
}
|
||||
if (!line.trim()) continue;
|
||||
const f = splitCsvLine(line);
|
||||
const get = (col: string) => f[idx[col]]?.trim() ?? "";
|
||||
|
||||
const id = get("id");
|
||||
const isim = get("isim");
|
||||
const universite = get("universite");
|
||||
const tur = get("tur");
|
||||
if (!id || !isim || !universite || !tur) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push([
|
||||
id,
|
||||
isim,
|
||||
universite,
|
||||
get("unitur") || null,
|
||||
get("il") || null,
|
||||
get("fakulte") || null,
|
||||
tur,
|
||||
toInt(get("sure")),
|
||||
toInt(get("onlisans")) ?? 0,
|
||||
...YEARS.flatMap((y) => [
|
||||
positiveOrNull(toInt(get(`sira${y}`))),
|
||||
positiveOrNull(toReal(get(`puan${y}`))),
|
||||
toInt(get(`kontenjan${y}`)),
|
||||
toInt(get(`yerlesen${y}`)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
const insertAll = db.transaction((all: unknown[][]) => {
|
||||
for (const r of all) insert.run(...r);
|
||||
});
|
||||
insertAll(rows);
|
||||
|
||||
const count = db
|
||||
.prepare("SELECT COUNT(*) AS c FROM programs")
|
||||
.get() as { c: number };
|
||||
const withSira = db
|
||||
.prepare("SELECT COUNT(*) AS c FROM programs WHERE sira2024 IS NOT NULL")
|
||||
.get() as { c: number };
|
||||
console.log(`Yüklendi: ${count.c} program (${skipped} satır atlandı)`);
|
||||
console.log(`2024 sıralaması olan: ${withSira.c}`);
|
||||
console.log(`DB: ${dbPath}`);
|
||||
}
|
||||
|
||||
main().then(() => db.close());
|
||||
Reference in New Issue
Block a user