Compare commits
7 Commits
ea85a053c5
...
24f53ddbce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24f53ddbce | ||
|
|
e0ff9dec0a | ||
|
|
14f9b622b0 | ||
|
|
0c6c9da56c | ||
|
|
33a260017a | ||
|
|
1e810e1782 | ||
|
|
34c8f90e26 |
@@ -33,7 +33,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
|||||||
# V8 heap sınırı ana süreç + statik üretim worker'larına miras kalır.
|
# V8 heap sınırı ana süreç + statik üretim worker'larına miras kalır.
|
||||||
# Turbopack'ın Rust tarafı bu sınırın dışında; onun hedefi next.config.ts'te
|
# Turbopack'ın Rust tarafı bu sınırın dışında; onun hedefi next.config.ts'te
|
||||||
# turbopackMemoryLimit ile verilir (BUILD_KAYNAK_KISITLI bloğu).
|
# turbopackMemoryLimit ile verilir (BUILD_KAYNAK_KISITLI bloğu).
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=1536"
|
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||||
ENV BUILD_KAYNAK_KISITLI=1
|
ENV BUILD_KAYNAK_KISITLI=1
|
||||||
RUN --mount=type=cache,id=next-cache,target=/app/.next/cache \
|
RUN --mount=type=cache,id=next-cache,target=/app/.next/cache \
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|||||||
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
BIN
data/yokatlas.db
BIN
data/yokatlas.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
drizzle/0001_steady_donald_blake.sql
Normal file
3
drizzle/0001_steady_donald_blake.sql
Normal file
@@ -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`);
|
||||||
907
drizzle/meta/0001_snapshot.json
Normal file
907
drizzle/meta/0001_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1786053613118,
|
"when": 1786053613118,
|
||||||
"tag": "0000_baslangic",
|
"tag": "0000_baslangic",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1786223252523,
|
||||||
|
"tag": "0001_steady_donald_blake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,10 @@ const nextConfig: NextConfig = {
|
|||||||
...(kaynakKisitli
|
...(kaynakKisitli
|
||||||
? {
|
? {
|
||||||
// Turbopack'ın Rust tarafı NODE_OPTIONS heap sınırını görmez; bellek
|
// Turbopack'ın Rust tarafı NODE_OPTIONS heap sınırını görmez; bellek
|
||||||
// hedefi byte cinsinden ayrıca verilir (1.5 GB).
|
// hedefi byte cinsinden ayrıca verilir (1 GB). 1.5 GB hedef + Node
|
||||||
turbopackMemoryLimit: 1_610_612_736,
|
// heap'i, ~1.5 GB boş belleği olan VPS'te derleme fazında OOM-kill
|
||||||
|
// üretiyordu (lokalde --memory=1536m ile reprodüke edildi).
|
||||||
|
turbopackMemoryLimit: 1_073_741_824,
|
||||||
// Statik üretim: varsayılan 4 worker × 8 eşzamanlı sayfa yerine tek
|
// Statik üretim: varsayılan 4 worker × 8 eşzamanlı sayfa yerine tek
|
||||||
// worker × 4 sayfa. Her worker ayrı bir Node süreci/heap'idir.
|
// worker × 4 sayfa. Her worker ayrı bir Node süreci/heap'idir.
|
||||||
cpus: 1,
|
cpus: 1,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
|
import { uniAdiNormalize, uniturOnar } from "./unitur-onar";
|
||||||
|
|
||||||
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
const API = "https://yokatlas.yok.gov.tr/api/tercih-kilavuz/search";
|
||||||
const PAGE_SIZE = 500;
|
const PAGE_SIZE = 500;
|
||||||
@@ -169,7 +170,9 @@ async function main() {
|
|||||||
insert.run(
|
insert.run(
|
||||||
id,
|
id,
|
||||||
r.birimAdi,
|
r.birimAdi,
|
||||||
r.universiteAdi,
|
// "ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu varyantlar
|
||||||
|
// duplikat kurum adı üretmesin
|
||||||
|
uniAdiNormalize(r.universiteAdi),
|
||||||
r.universiteTuru ?? null,
|
r.universiteTuru ?? null,
|
||||||
r.ilAdi ?? null,
|
r.ilAdi ?? null,
|
||||||
r.fymkAdi ?? null,
|
r.fymkAdi ?? null,
|
||||||
@@ -205,6 +208,11 @@ async function main() {
|
|||||||
console.log(
|
console.log(
|
||||||
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
|
`DB toplam: ${stats.toplam} program, 2025 sıralaması olan: ${stats.sirali}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// API'nin universiteTuru döndürmediği kayıtlar devlet/vakıf filtrelerinden
|
||||||
|
// düşmesin: her tazelemeden sonra unitur onarımı koşulur (idempotent).
|
||||||
|
db.transaction(() => uniturOnar(db))();
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel")
|
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel") ×
|
||||||
* kombinasyonları için 1'er örnek tercih satırı (gerekce/riskNotu/trendOzeti)
|
* (üniversite tipi: genel/devlet/vakıf) kombinasyonları için 1'er örnek
|
||||||
* üretip app.db'deki tadimlik_havuzu tablosuna yazar.
|
* tercih satırı (gerekce/riskNotu/trendOzeti) üretip app.db'deki
|
||||||
|
* tadimlik_havuzu tablosuna yazar.
|
||||||
*
|
*
|
||||||
* pnpm tadimlik # boş anahtarları doldurur (idempotent)
|
* pnpm tadimlik # boş anahtarları doldurur (idempotent)
|
||||||
* pnpm tadimlik --force # mevcut satırları da yeniden üretir
|
* pnpm tadimlik --force # mevcut satırları da yeniden üretir
|
||||||
* pnpm tadimlik --kova=10-50k --tur=say --kategori=muhendislik-teknoloji
|
* 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 --mock # anahtar olsa da mock metin (dev/test)
|
||||||
* pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms)
|
* pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms)
|
||||||
*
|
*
|
||||||
@@ -24,10 +26,16 @@ import {
|
|||||||
type DilimKey,
|
type DilimKey,
|
||||||
type Program,
|
type Program,
|
||||||
type PuanTuruKey,
|
type PuanTuruKey,
|
||||||
|
type UniturGrubu,
|
||||||
} from "../src/types/yokatlas";
|
} from "../src/types/yokatlas";
|
||||||
import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler";
|
import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler";
|
||||||
import { havuzuKompaktJson, turLabel } from "../src/lib/rapor-havuzu";
|
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 { anahtarVar } from "../src/lib/ai/client";
|
||||||
import { yapilandirilmisUret } from "../src/lib/ai/cagri";
|
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
|
* 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.
|
* (boşsa "garanti") kategoriye ve üniversite tipine uyan, 2025'te en çok
|
||||||
* Tanınır/dolu programlar tadımlıkta daha ikna edici.
|
* yerleşen alan program. Tanınır/dolu programlar tadımlıkta daha ikna edici.
|
||||||
*/
|
*/
|
||||||
function programSec(
|
function programSec(
|
||||||
temsiliSira: number,
|
temsiliSira: number,
|
||||||
tur: PuanTuruKey,
|
tur: PuanTuruKey,
|
||||||
kategori: string | null,
|
kategori: string | null,
|
||||||
|
uniturGrubu?: UniturGrubu,
|
||||||
): { program: Program; dilim: DilimKey } | null {
|
): { program: Program; dilim: DilimKey } | null {
|
||||||
for (const dilim of ["dengeli", "garanti"] as const) {
|
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
|
const uygunlar = kategori
|
||||||
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori]))
|
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori], p.fakulte))
|
||||||
: programlar;
|
: programlar;
|
||||||
if (uygunlar.length === 0) continue;
|
if (uygunlar.length === 0) continue;
|
||||||
const secilen = [...uygunlar].sort(
|
const secilen = [...uygunlar].sort(
|
||||||
@@ -152,6 +164,7 @@ async function main() {
|
|||||||
const kovaFiltre = bayrak("kova");
|
const kovaFiltre = bayrak("kova");
|
||||||
const turFiltre = bayrak("tur");
|
const turFiltre = bayrak("tur");
|
||||||
const kategoriFiltre = bayrak("kategori");
|
const kategoriFiltre = bayrak("kategori");
|
||||||
|
const tipFiltre = bayrak("tip");
|
||||||
|
|
||||||
const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter(
|
const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter(
|
||||||
(t) => !turFiltre || t === turFiltre,
|
(t) => !turFiltre || t === turFiltre,
|
||||||
@@ -161,6 +174,9 @@ async function main() {
|
|||||||
...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })),
|
...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })),
|
||||||
].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre);
|
].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre);
|
||||||
const kovalar = KOVALAR.filter((k) => !kovaFiltre || k.slug === kovaFiltre);
|
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(
|
const mevcutlar = new Set(
|
||||||
(
|
(
|
||||||
@@ -169,9 +185,10 @@ async function main() {
|
|||||||
kovaSlug: schema.tadimlikHavuzu.kovaSlug,
|
kovaSlug: schema.tadimlikHavuzu.kovaSlug,
|
||||||
tur: schema.tadimlikHavuzu.tur,
|
tur: schema.tadimlikHavuzu.tur,
|
||||||
kategoriSlug: schema.tadimlikHavuzu.kategoriSlug,
|
kategoriSlug: schema.tadimlikHavuzu.kategoriSlug,
|
||||||
|
tip: schema.tadimlikHavuzu.tip,
|
||||||
})
|
})
|
||||||
.from(schema.tadimlikHavuzu)
|
.from(schema.tadimlikHavuzu)
|
||||||
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`),
|
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}|${s.tip}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mockAktif()) {
|
if (mockAktif()) {
|
||||||
@@ -185,13 +202,19 @@ async function main() {
|
|||||||
for (const kova of kovalar) {
|
for (const kova of kovalar) {
|
||||||
for (const tur of turlar) {
|
for (const tur of turlar) {
|
||||||
for (const kategori of kategoriler) {
|
for (const kategori of kategoriler) {
|
||||||
const anahtar = `${kova.slug}|${tur}|${kategori.slug}`;
|
for (const tip of tipler) {
|
||||||
|
const anahtar = `${kova.slug}|${tur}|${kategori.slug}|${tip}`;
|
||||||
if (!force && mevcutlar.has(anahtar)) {
|
if (!force && mevcutlar.has(anahtar)) {
|
||||||
atlananMevcut++;
|
atlananMevcut++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secim = programSec(kova.temsiliSira, tur, kategori.ad);
|
const secim = programSec(
|
||||||
|
kova.temsiliSira,
|
||||||
|
tur,
|
||||||
|
kategori.ad,
|
||||||
|
tip === "genel" ? undefined : tip,
|
||||||
|
);
|
||||||
if (!secim) {
|
if (!secim) {
|
||||||
atlananBos++;
|
atlananBos++;
|
||||||
continue;
|
continue;
|
||||||
@@ -218,6 +241,7 @@ async function main() {
|
|||||||
kovaSlug: kova.slug,
|
kovaSlug: kova.slug,
|
||||||
tur,
|
tur,
|
||||||
kategoriSlug: kategori.slug,
|
kategoriSlug: kategori.slug,
|
||||||
|
tip,
|
||||||
programId: secim.program.id,
|
programId: secim.program.id,
|
||||||
dilim: secim.dilim,
|
dilim: secim.dilim,
|
||||||
gerekce: metin.gerekce,
|
gerekce: metin.gerekce,
|
||||||
@@ -232,6 +256,7 @@ async function main() {
|
|||||||
schema.tadimlikHavuzu.kovaSlug,
|
schema.tadimlikHavuzu.kovaSlug,
|
||||||
schema.tadimlikHavuzu.tur,
|
schema.tadimlikHavuzu.tur,
|
||||||
schema.tadimlikHavuzu.kategoriSlug,
|
schema.tadimlikHavuzu.kategoriSlug,
|
||||||
|
schema.tadimlikHavuzu.tip,
|
||||||
],
|
],
|
||||||
set: {
|
set: {
|
||||||
programId: secim.program.id,
|
programId: secim.program.id,
|
||||||
@@ -246,7 +271,9 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
uretilen++;
|
uretilen++;
|
||||||
console.log(`✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`);
|
console.log(
|
||||||
|
`✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`,
|
||||||
|
);
|
||||||
// Gerçek LLM'de rate limit dostu ardışık tempo
|
// Gerçek LLM'de rate limit dostu ardışık tempo
|
||||||
if (!mockAktif()) {
|
if (!mockAktif()) {
|
||||||
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
|
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
|
||||||
@@ -255,6 +282,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Bitti: ${uretilen} üretildi, ${atlananMevcut} mevcut atlandı, ${atlananBos} içeriksiz/şemasız atlandı.`,
|
`Bitti: ${uretilen} üretildi, ${atlananMevcut} mevcut atlandı, ${atlananBos} içeriksiz/şemasız atlandı.`,
|
||||||
|
|||||||
158
scripts/unitur-onar.ts
Normal file
158
scripts/unitur-onar.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* unitur (üniversite tipi) NULL onarımı + üniversite adı normalizasyonu.
|
||||||
|
*
|
||||||
|
* YÖK Atlas API'si bazı kayıtlarda universiteTuru döndürmüyor; DB'de ~2.081
|
||||||
|
* satır unitur=NULL kalıyor ve bunlar hem devlet/vakıf filtrelerinden hem
|
||||||
|
* sihirbaz sayaçlarından sessizce düşüyor. Onarım üç aşamalı:
|
||||||
|
*
|
||||||
|
* A) Aynı üniversitenin dolu satırından kopyala (Nişantaşı, Beykent…)
|
||||||
|
* B) Kalanlarda il KKTC yerleşimiyse unitur='KKTC'
|
||||||
|
* C) Küçük manuel eşleme (Cerrahpaşa, MSB/İçişleri, Alanya, Manas…)
|
||||||
|
*
|
||||||
|
* Ayrıca "İSTANBUL ÜNİVERSİTESİ- CERRAHPAŞA" gibi tire etrafı boşluklu
|
||||||
|
* duplikat adlar normalize edilir; refresh.ts insert yolunda da aynı
|
||||||
|
* normalizasyon uygulanır ki duplikat bir daha oluşmasın.
|
||||||
|
*
|
||||||
|
* İdempotenttir; yalnızca NULL satırlara dokunur.
|
||||||
|
*
|
||||||
|
* Kullanım: npx tsx scripts/unitur-onar.ts (dev sunucu KAPALIYKEN —
|
||||||
|
* uygulama DB'yi readonly açar ama WAL checkpoint için tek yazar olmalı)
|
||||||
|
*/
|
||||||
|
import path from "node:path";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
/** Tire etrafı boşluk + çift boşluk temizliği (üniversite adı için). */
|
||||||
|
export function uniAdiNormalize(ad: string): string {
|
||||||
|
return ad.replace(/\s*-\s*/g, "-").replace(/\s{2,}/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const KKTC_ILLERI = [
|
||||||
|
"GİRNE",
|
||||||
|
"LEFKOŞA",
|
||||||
|
"GAZİMAĞUSA",
|
||||||
|
"LEFKE",
|
||||||
|
"GÜZELYURT",
|
||||||
|
"KIBRIS",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Hiçbir satırında unitur olmayan ve il'den de çıkarılamayan kurumlar.
|
||||||
|
const MANUEL_UNITUR: Record<string, string> = {
|
||||||
|
"İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "DEVLET",
|
||||||
|
"İÇİŞLERİ BAKANLIĞI VE MİLLİ SAVUNMA BAKANLIĞI ADINA SAĞLIK BİLİMLERİ ÜNİVERSİTESİNDE EĞİTİM ALACAKLAR":
|
||||||
|
"DEVLET",
|
||||||
|
"ALANYA ÜNİVERSİTESİ": "VAKIF",
|
||||||
|
"KIRGIZİSTAN-TÜRKİYE MANAS ÜNİVERSİTESİ": "YURTDISI KAMU",
|
||||||
|
"HOCA AHMET YESEVİ ULUSLARARASI TÜRK-KAZAK ÜNİVERSİTESİ": "YURTDISI KAMU",
|
||||||
|
"AZERBAYCAN DEVLET PEDAGOJİ ÜNİVERSİTESİ": "YURTDISI KAMU",
|
||||||
|
};
|
||||||
|
|
||||||
|
// İl'i hiç olmayan kampüsler için ad → il eşlemesi (yalnızca kesin olanlar).
|
||||||
|
const MANUEL_IL: Record<string, string> = {
|
||||||
|
"İSTANBUL ÜNİVERSİTESİ-CERRAHPAŞA": "İSTANBUL",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function uniturOnar(db: Database.Database): void {
|
||||||
|
// 0) Üniversite adı normalizasyonu (Cerrahpaşa duplikatını birleştirir)
|
||||||
|
const adlar = db
|
||||||
|
.prepare("SELECT DISTINCT universite FROM programs")
|
||||||
|
.all() as { universite: string }[];
|
||||||
|
const adDuzelt = db.prepare(
|
||||||
|
"UPDATE programs SET universite = ? WHERE universite = ?"
|
||||||
|
);
|
||||||
|
let duzeltilen = 0;
|
||||||
|
for (const { universite } of adlar) {
|
||||||
|
const temiz = uniAdiNormalize(universite);
|
||||||
|
if (temiz !== universite) {
|
||||||
|
adDuzelt.run(temiz, universite);
|
||||||
|
duzeltilen++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (duzeltilen > 0) console.log(`Ad normalizasyonu: ${duzeltilen} kurum`);
|
||||||
|
|
||||||
|
// A) Aynı üniversitenin dolu satırından kopyala. Birden fazla FARKLI dolu
|
||||||
|
// değer varsa sessizce seçim yapma — kaynak veride tutarsızlık demektir.
|
||||||
|
const celiskiler = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT universite, COUNT(DISTINCT unitur) AS n FROM programs
|
||||||
|
WHERE unitur IS NOT NULL GROUP BY universite HAVING n > 1`
|
||||||
|
)
|
||||||
|
.all() as { universite: string; n: number }[];
|
||||||
|
if (celiskiler.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Aynı üniversitede birden fazla unitur: ${celiskiler
|
||||||
|
.map((c) => c.universite)
|
||||||
|
.join(", ")} — önce kaynak veriyi kontrol et.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const aKopya = db
|
||||||
|
.prepare(
|
||||||
|
`UPDATE programs SET unitur = (
|
||||||
|
SELECT p2.unitur FROM programs p2
|
||||||
|
WHERE p2.universite = programs.universite AND p2.unitur IS NOT NULL
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE unitur IS NULL AND EXISTS (
|
||||||
|
SELECT 1 FROM programs p3
|
||||||
|
WHERE p3.universite = programs.universite AND p3.unitur IS NOT NULL
|
||||||
|
)`
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
console.log(`Aşama A (üniversiteden kopya): ${aKopya.changes} satır`);
|
||||||
|
|
||||||
|
// B) Kalan NULL'larda il KKTC yerleşimiyse
|
||||||
|
const bKktc = db
|
||||||
|
.prepare(
|
||||||
|
`UPDATE programs SET unitur = 'KKTC'
|
||||||
|
WHERE unitur IS NULL AND il IN (${KKTC_ILLERI.map(() => "?").join(",")})`
|
||||||
|
)
|
||||||
|
.run(...KKTC_ILLERI);
|
||||||
|
console.log(`Aşama B (KKTC ili): ${bKktc.changes} satır`);
|
||||||
|
|
||||||
|
// C) Manuel eşlemeler
|
||||||
|
const cUnitur = db.prepare(
|
||||||
|
"UPDATE programs SET unitur = ? WHERE unitur IS NULL AND universite = ?"
|
||||||
|
);
|
||||||
|
let cToplam = 0;
|
||||||
|
for (const [ad, tur] of Object.entries(MANUEL_UNITUR)) {
|
||||||
|
cToplam += cUnitur.run(tur, ad).changes;
|
||||||
|
}
|
||||||
|
console.log(`Aşama C (manuel): ${cToplam} satır`);
|
||||||
|
|
||||||
|
const ilDuzelt = db.prepare(
|
||||||
|
"UPDATE programs SET il = ? WHERE il IS NULL AND universite = ?"
|
||||||
|
);
|
||||||
|
let ilToplam = 0;
|
||||||
|
for (const [ad, il] of Object.entries(MANUEL_IL)) {
|
||||||
|
ilToplam += ilDuzelt.run(il, ad).changes;
|
||||||
|
}
|
||||||
|
if (ilToplam > 0) console.log(`İl doldurma (manuel): ${ilToplam} satır`);
|
||||||
|
|
||||||
|
// Rapor: kalan NULL'lar
|
||||||
|
const kalan = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT universite, COUNT(*) AS adet FROM programs
|
||||||
|
WHERE unitur IS NULL GROUP BY universite ORDER BY adet DESC`
|
||||||
|
)
|
||||||
|
.all() as { universite: string; adet: number }[];
|
||||||
|
if (kalan.length === 0) {
|
||||||
|
console.log("Kalan NULL unitur: 0 ✓");
|
||||||
|
} else {
|
||||||
|
console.log(`Kalan NULL unitur (${kalan.length} kurum):`);
|
||||||
|
for (const k of kalan) console.log(` ${k.universite}: ${k.adet}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doğrudan koşulduğunda (npx tsx scripts/unitur-onar.ts)
|
||||||
|
if (process.argv[1]?.endsWith("unitur-onar.ts")) {
|
||||||
|
const db = new Database(path.join(process.cwd(), "data", "yokatlas.db"));
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
db.transaction(() => uniturOnar(db))();
|
||||||
|
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||||
|
const dagilim = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT COALESCE(unitur,'(NULL)') AS t, COUNT(*) AS n FROM programs GROUP BY unitur ORDER BY n DESC"
|
||||||
|
)
|
||||||
|
.all() as { t: string; n: number }[];
|
||||||
|
console.log("Dağılım:", dagilim.map((d) => `${d.t}=${d.n}`).join(" "));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
@@ -36,9 +36,19 @@ export function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
return NextResponse.json({ error: "Geçersiz offset." }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /sonuc tablosunun filtreleri sayfalamada da korunur (bkz. sonucHref)
|
||||||
|
const iller = [
|
||||||
|
...new Set(searchParams.getAll("il").filter((i) => i.trim().length > 0)),
|
||||||
|
].slice(0, 5);
|
||||||
|
const tipParam = searchParams.get("tip");
|
||||||
|
const uniturGrubu =
|
||||||
|
tipParam === "devlet" || tipParam === "vakif" ? tipParam : undefined;
|
||||||
|
|
||||||
const { programlar, toplam } = dilimAra(sira, tur, dilimParam as DilimKey, {
|
const { programlar, toplam } = dilimAra(sira, tur, dilimParam as DilimKey, {
|
||||||
limit: SAYFA_BOYU,
|
limit: SAYFA_BOYU,
|
||||||
offset,
|
offset,
|
||||||
|
iller,
|
||||||
|
uniturGrubu,
|
||||||
});
|
});
|
||||||
// Tablo satırının kullanmadığı alanlar payload'a girmesin
|
// Tablo satırının kullanmadığı alanlar payload'a girmesin
|
||||||
return NextResponse.json({ programlar: programlar.map(programOzetle), toplam });
|
return NextResponse.json({ programlar: programlar.map(programOzetle), toplam });
|
||||||
|
|||||||
@@ -29,7 +29,18 @@ export async function GET(istek: Request) {
|
|||||||
? kategoriParam
|
? kategoriParam
|
||||||
: undefined;
|
: 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(
|
return NextResponse.json(
|
||||||
{ tadimlik },
|
{ tadimlik },
|
||||||
// Satırlar kova granülaritesinde statik; 1 saat cache yeterli.
|
// Satırlar kova granülaritesinde statik; 1 saat cache yeterli.
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function SonucPage({
|
|||||||
tur?: string;
|
tur?: string;
|
||||||
sihirbaz?: string;
|
sihirbaz?: string;
|
||||||
hazir?: string;
|
hazir?: string;
|
||||||
|
il?: string | string[];
|
||||||
|
tip?: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +53,22 @@ export default function SonucPage({
|
|||||||
params.tur && params.tur in PUAN_TURLERI
|
params.tur && params.tur in PUAN_TURLERI
|
||||||
? (params.tur as PuanTuruKey)
|
? (params.tur as PuanTuruKey)
|
||||||
: "say";
|
: "say";
|
||||||
|
// Sihirbaz seçimlerinden gelen tablo filtreleri (bkz. sonucHref):
|
||||||
|
// il tekrarlı param, tip devlet|vakif allowlist'li
|
||||||
|
const iller = [
|
||||||
|
...new Set(
|
||||||
|
(Array.isArray(params.il)
|
||||||
|
? params.il
|
||||||
|
: params.il
|
||||||
|
? [params.il]
|
||||||
|
: []
|
||||||
|
).filter((i) => i.trim().length > 0),
|
||||||
|
),
|
||||||
|
].slice(0, 5);
|
||||||
|
const uniturGrubu =
|
||||||
|
params.tip === "devlet" || params.tip === "vakif"
|
||||||
|
? params.tip
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) {
|
||||||
return (
|
return (
|
||||||
@@ -71,8 +89,14 @@ export default function SonucPage({
|
|||||||
|
|
||||||
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
|
||||||
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
sayfanın ana gövdesi; puan türü navbar'daki sıralama
|
||||||
rozetinden değiştirilir */}
|
rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/
|
||||||
<SonucProgramTablosu sira={sira} tur={turKey} />
|
vakıf) URL paramlarıyla tabloya uygulanır. */}
|
||||||
|
<SonucProgramTablosu
|
||||||
|
sira={sira}
|
||||||
|
tur={turKey}
|
||||||
|
iller={iller}
|
||||||
|
uniturGrubu={uniturGrubu}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
||||||
Havuzda içerik yoksa bileşen hiç render edilmez. */}
|
Havuzda içerik yoksa bileşen hiç render edilmez. */}
|
||||||
|
|||||||
@@ -94,21 +94,28 @@ export function ProgramTablosu({
|
|||||||
sonuclar,
|
sonuclar,
|
||||||
adaySira,
|
adaySira,
|
||||||
tur,
|
tur,
|
||||||
|
filtreIller,
|
||||||
|
filtreTip,
|
||||||
}: {
|
}: {
|
||||||
sonuclar: RankResultsListe;
|
sonuclar: RankResultsListe;
|
||||||
adaySira: number;
|
adaySira: number;
|
||||||
tur: PuanTuruKey;
|
tur: PuanTuruKey;
|
||||||
|
filtreIller?: string[];
|
||||||
|
filtreTip?: "devlet" | "vakif";
|
||||||
}) {
|
}) {
|
||||||
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
const [aktifDilim, setAktifDilim] = useState<DilimKey>(() =>
|
||||||
varsayilanDilim(sonuclar),
|
varsayilanDilim(sonuclar),
|
||||||
);
|
);
|
||||||
|
// Sihirbaz seçimlerinden URL'e yazılan filtreler (bkz. sonucHref) —
|
||||||
|
// sayfalama isteklerinde ve veri anahtarında kullanılır.
|
||||||
|
const iller = useMemo(() => filtreIller ?? [], [filtreIller]);
|
||||||
|
|
||||||
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür değişince
|
// "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür/filtre
|
||||||
// (puan türü seçici aynı bileşeni yeniden render eder, remount etmez)
|
// değişince (puan türü seçici aynı bileşeni yeniden render eder, remount
|
||||||
// render sırasında sıfırlanır ki eski türün satırları yenisine karışmasın.
|
// etmez) render sırasında sıfırlanır ki eski kümenin satırları karışmasın.
|
||||||
const [ekstra, setEkstra] = useState(BOS_EKSTRA);
|
const [ekstra, setEkstra] = useState(BOS_EKSTRA);
|
||||||
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
|
const [yuklenen, setYuklenen] = useState<DilimKey | null>(null);
|
||||||
const veriAnahtari = `${adaySira}-${tur}`;
|
const veriAnahtari = `${adaySira}-${tur}-${iller.join(",")}-${filtreTip ?? ""}`;
|
||||||
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
|
const [oncekiAnahtar, setOncekiAnahtar] = useState(veriAnahtari);
|
||||||
if (veriAnahtari !== oncekiAnahtar) {
|
if (veriAnahtari !== oncekiAnahtar) {
|
||||||
setOncekiAnahtar(veriAnahtari);
|
setOncekiAnahtar(veriAnahtari);
|
||||||
@@ -129,9 +136,15 @@ export function ProgramTablosu({
|
|||||||
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
async function dahaFazlaYukle(dilim: DilimKey, offset: number) {
|
||||||
setYuklenen(dilim);
|
setYuklenen(dilim);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const q = new URLSearchParams({
|
||||||
`/api/programlar?sira=${adaySira}&tur=${tur}&dilim=${dilim}&offset=${offset}`,
|
sira: String(adaySira),
|
||||||
);
|
tur,
|
||||||
|
dilim,
|
||||||
|
offset: String(offset),
|
||||||
|
});
|
||||||
|
for (const il of iller) q.append("il", il);
|
||||||
|
if (filtreTip) q.set("tip", filtreTip);
|
||||||
|
const res = await fetch(`/api/programlar?${q}`);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const veri = (await res.json()) as { programlar: ProgramListeSatiri[] };
|
const veri = (await res.json()) as { programlar: ProgramListeSatiri[] };
|
||||||
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
|
// Aynı sayfanın iki kez istenmesine karşı id bazında tekilleştir
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import { siraylaProgramAra } from "../liste-queries";
|
import { siraylaProgramAra } from "../liste-queries";
|
||||||
import { ProgramTablosu } from "./program-tablosu";
|
import { ProgramTablosu } from "./program-tablosu";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { PuanTuruKey } from "@/types/yokatlas";
|
import type { PuanTuruKey, UniturGrubu } from "@/types/yokatlas";
|
||||||
|
|
||||||
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
||||||
// yalnızca kompoze eder.
|
// yalnızca kompoze eder.
|
||||||
export function SonucProgramTablosu({
|
export function SonucProgramTablosu({
|
||||||
sira,
|
sira,
|
||||||
tur,
|
tur,
|
||||||
|
iller,
|
||||||
|
uniturGrubu,
|
||||||
}: {
|
}: {
|
||||||
sira: number;
|
sira: number;
|
||||||
tur: PuanTuruKey;
|
tur: PuanTuruKey;
|
||||||
|
iller?: string[];
|
||||||
|
uniturGrubu?: UniturGrubu;
|
||||||
}) {
|
}) {
|
||||||
const sonuclar = siraylaProgramAra(sira, tur);
|
const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu });
|
||||||
return <ProgramTablosu sonuclar={sonuclar} adaySira={sira} tur={tur} />;
|
return (
|
||||||
|
<ProgramTablosu
|
||||||
|
sonuclar={sonuclar}
|
||||||
|
adaySira={sira}
|
||||||
|
tur={tur}
|
||||||
|
filtreIller={iller}
|
||||||
|
filtreTip={uniturGrubu}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SonucProgramTablosuSkeleton() {
|
export function SonucProgramTablosuSkeleton() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ProgramListeSatiri,
|
ProgramListeSatiri,
|
||||||
PuanTuruKey,
|
PuanTuruKey,
|
||||||
RankResultsListe,
|
RankResultsListe,
|
||||||
|
UniturGrubu,
|
||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
|
|
||||||
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin.
|
// Tabloda kullanılmayan alanlar (fakulte, puan2025) client'a serileşmesin.
|
||||||
@@ -29,13 +30,22 @@ export function programOzetle(p: Program): ProgramListeSatiri {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// /sonuc'un ham tablosu: yalnızca sıralamayla ulaşılabilen programlar,
|
// /sonuc'un ham tablosu: sıralamayla ulaşılabilen programlar,
|
||||||
// hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır.
|
// hayal/dengeli/garanti dilimli, dilim başına ilk 20 satır. Sihirbaz
|
||||||
|
// seçimlerinden il + üniversite tipi URL üzerinden buraya taşınır ve SQL'de
|
||||||
|
// filtrelenir; kategori bilinçli olarak uygulanmaz (DB'de kategori kolonu
|
||||||
|
// yok, JS filtresi offset/toplam semantiğini bozar — kategori AI listesi ve
|
||||||
|
// tadımlıkta uygulanır).
|
||||||
export function siraylaProgramAra(
|
export function siraylaProgramAra(
|
||||||
sira: number,
|
sira: number,
|
||||||
tur: PuanTuruKey,
|
tur: PuanTuruKey,
|
||||||
|
opts: { iller?: string[]; uniturGrubu?: UniturGrubu } = {},
|
||||||
): RankResultsListe {
|
): RankResultsListe {
|
||||||
const ham = searchByRank(sira, tur, { limitPerBucket: 20 });
|
const ham = searchByRank(sira, tur, {
|
||||||
|
limitPerBucket: 20,
|
||||||
|
iller: opts.iller,
|
||||||
|
uniturGrubu: opts.uniturGrubu,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
hayal: ham.hayal.map(programOzetle),
|
hayal: ham.hayal.map(programOzetle),
|
||||||
dengeli: ham.dengeli.map(programOzetle),
|
dengeli: ham.dengeli.map(programOzetle),
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ config:
|
|||||||
look: handDrawn
|
look: handDrawn
|
||||||
---
|
---
|
||||||
flowchart TD
|
flowchart TD
|
||||||
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>yalnızca ulaşabildiğin alanlar listelenir")
|
T("Sıralamanla açılan tüm programlar") --> A("1. adım · Alan seçimi<br/>alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, yalnızca ulaşabildiklerin listelenir")
|
||||||
A --> B("2. adım · İl seçimi<br/>iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
A --> B("2. adım · İl seçimi<br/>programı olan tüm iller, seçtiğin alanlardaki program sayısına göre yeniden sayılır")
|
||||||
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
B --> C("3. adım · Devlet / Vakıf<br/>sayılar hem alanı hem ili hesaba katar")
|
||||||
C --> H("Aday havuzu<br/>elde kalan gerçek programlar")
|
C --> H("Aday havuzu<br/>elde kalan gerçek programlar")
|
||||||
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
H --> S{"Havuzda 24'ten az program mı kaldı?"}
|
||||||
@@ -42,7 +42,7 @@ export function HuniSemasi() {
|
|||||||
return (
|
return (
|
||||||
<ElCizimiSema
|
<ElCizimiSema
|
||||||
tanim={SEMA}
|
tanim={SEMA}
|
||||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||||
figcaption={
|
figcaption={
|
||||||
<>
|
<>
|
||||||
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||||
|
|||||||
@@ -107,11 +107,14 @@ export function MeraklisinaIcerik() {
|
|||||||
“Yapay Zeka listemi kur” sihirbazındaki üç adım birbirinden
|
“Yapay Zeka listemi kur” sihirbazındaki üç adım birbirinden
|
||||||
bağımsız değildir. İlk adımda yalnızca{" "}
|
bağımsız değildir. İlk adımda yalnızca{" "}
|
||||||
<strong>sıralamanla ulaşabildiğin</strong>{" "}
|
<strong>sıralamanla ulaşabildiğin</strong>{" "}
|
||||||
alanlar listelenir; ikinci adımdaki iller, seçtiğin alanlardaki
|
alanlar listelenir — bir bölümün hangi alana girdiği, adına ve
|
||||||
program sayısına göre yeniden hesaplanır; üçüncü adımdaki
|
bağlı olduğu fakülteye bakılarak belirlenir. İkinci adımda
|
||||||
|
programı olan tüm iller, seçtiğin alanlardaki program sayısına
|
||||||
|
göre yeniden hesaplanarak listelenir; üçüncü adımdaki
|
||||||
devlet/vakıf sayıları da hem alan hem il seçimini hesaba katar.
|
devlet/vakıf sayıları da hem alan hem il seçimini hesaba katar.
|
||||||
Adımlar bitince elde kalan gerçek programlar bir{" "}
|
Seçtiğin il ve devlet/vakıf tercihi, sonuç sayfasındaki ücretsiz
|
||||||
<strong>aday havuzu</strong>{" "}
|
tabloya da uygulanır. Adımlar bitince elde kalan gerçek
|
||||||
|
programlar bir <strong>aday havuzu</strong>{" "}
|
||||||
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
olur ve yapay zekâya öyle gider: ona boş bir sayfa değil, bu havuz
|
||||||
verilir. Aşağıdaki şema akışın tamamını gösterir — havuz
|
verilir. Aşağıdaki şema akışın tamamını gösterir — havuz
|
||||||
24'ün altına düşerse filtrelerin nasıl gevşetildiği, yine de
|
24'ün altına düşerse filtrelerin nasıl gevşetildiği, yine de
|
||||||
|
|||||||
@@ -27,10 +27,17 @@ import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
|||||||
import { turkishSlugify } from "@/lib/slug";
|
import { turkishSlugify } from "@/lib/slug";
|
||||||
import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
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";
|
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({
|
export function TadimlikSatiri({
|
||||||
sira,
|
sira,
|
||||||
tur,
|
tur,
|
||||||
@@ -42,29 +49,46 @@ export function TadimlikSatiri({
|
|||||||
ilk: TadimlikSatir | null;
|
ilk: TadimlikSatir | null;
|
||||||
}) {
|
}) {
|
||||||
const [satir, setSatir] = useState<TadimlikSatir | null>(ilk);
|
const [satir, setSatir] = useState<TadimlikSatir | null>(ilk);
|
||||||
const yuklenenSlug = useRef(GENEL);
|
const yuklenenAnahtar = useRef(`${GENEL}|${GENEL}`);
|
||||||
|
|
||||||
// Sihirbaz kategorisi yalnızca localStorage'da yaşar; mount'ta ve profil
|
// Sihirbaz kategorisi ve devlet/vakıf tercihi yalnızca localStorage'da
|
||||||
// değiştikçe okunur, kategoriye özel satır sunucudan çekilir.
|
// 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(() => {
|
useEffect(() => {
|
||||||
const guncelle = () => {
|
const guncelle = () => {
|
||||||
const profil = tercihProfiliOku();
|
const profil = tercihProfiliOku();
|
||||||
const kategori = profil?.secimler.kategoriler[0];
|
const kategori = profil?.secimler.kategoriler[0];
|
||||||
const slug = kategori ? turkishSlugify(kategori) : GENEL;
|
const slug = kategori ? turkishSlugify(kategori) : GENEL;
|
||||||
if (slug === yuklenenSlug.current) return;
|
const tipTercihi = profil?.secimler.universiteTipi;
|
||||||
yuklenenSlug.current = slug;
|
const tip: TadimlikTip =
|
||||||
if (slug === GENEL) {
|
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);
|
setSatir(ilk);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch(
|
// SSR "genel" satırı seçilen tipe uymuyorsa fetch bitene kadar gizle
|
||||||
`/api/tadimlik?sira=${sira}&tur=${encodeURIComponent(tur)}&kategori=${encodeURIComponent(slug)}`,
|
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((yanit) => (yanit.ok ? yanit.json() : null))
|
||||||
.then((veri: { tadimlik: TadimlikSatir | null } | null) => {
|
.then((veri: { tadimlik: TadimlikSatir | null } | null) => {
|
||||||
// Yarış koruması: bu arada başka kategori yüklendiyse dokunma
|
// Yarış koruması: bu arada başka seçim yüklendiyse dokunma
|
||||||
if (yuklenenSlug.current !== slug) return;
|
if (yuklenenAnahtar.current !== anahtar) return;
|
||||||
if (veri?.tadimlik) setSatir(veri.tadimlik);
|
if (veri?.tadimlik) {
|
||||||
|
setSatir(veri.tadimlik);
|
||||||
|
} else if (tip !== GENEL) {
|
||||||
|
// Uygun tipte satır yok: yanlış tip göstermektense gizle
|
||||||
|
setSatir(null);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// üniversite tipi sayıları kategori+il'e göre /api/facetler'den tazelenir.
|
// üniversite tipi sayıları kategori+il'e göre /api/facetler'den tazelenir.
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ArrowLeft, ArrowRight, X } from "lucide-react";
|
import { ArrowLeft, ArrowRight, Search, X } from "lucide-react";
|
||||||
import { useReducedMotion } from "motion/react";
|
import { useReducedMotion } from "motion/react";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -27,11 +27,14 @@ export function Cip({
|
|||||||
secili,
|
secili,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
kucuk = false,
|
||||||
}: {
|
}: {
|
||||||
secili: boolean;
|
secili: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
kucuk?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const boyut = kucuk ? "px-2.5 py-1 text-xs" : "px-3.5 py-2 text-sm";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -39,8 +42,8 @@ export function Cip({
|
|||||||
aria-pressed={secili}
|
aria-pressed={secili}
|
||||||
className={
|
className={
|
||||||
secili
|
secili
|
||||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3.5 py-2 text-sm font-medium text-white transition-colors duration-200"
|
? `cursor-pointer rounded-full border border-primary bg-primary font-medium text-white transition-colors duration-200 ${boyut}`
|
||||||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3.5 py-2 text-sm text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
: `cursor-pointer rounded-full border border-slate-200 bg-white text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 ${boyut}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -93,6 +96,36 @@ export function SihirbazAdimlar({
|
|||||||
() => [...canliFacetler.iller].sort((a, b) => b.adet - a.adet),
|
() => [...canliFacetler.iller].sort((a, b) => b.adet - a.adet),
|
||||||
[canliFacetler.iller],
|
[canliFacetler.iller],
|
||||||
);
|
);
|
||||||
|
// Türkiye illeri ana çip grubu; KKTC/yurtdışı kampüsler haritada olmadığı
|
||||||
|
// için ayrı alt grupta listelenir.
|
||||||
|
const turkiyeIlleri = useMemo(
|
||||||
|
() => siraliIller.filter((i) => HARITA_ILLERI.has(i.il)),
|
||||||
|
[siraliIller],
|
||||||
|
);
|
||||||
|
const disIller = useMemo(
|
||||||
|
() => siraliIller.filter((i) => !HARITA_ILLERI.has(i.il)),
|
||||||
|
[siraliIller],
|
||||||
|
);
|
||||||
|
// İl araması: Türkçe büyük/küçük harf ve şapkalı harf farklarına duyarsız
|
||||||
|
// eşleşme için iki taraf da normalizeIlAdi'dan geçirilir.
|
||||||
|
const [ilArama, setIlArama] = useState("");
|
||||||
|
const ilAramaNorm = normalizeIlAdi(ilArama.trim());
|
||||||
|
const gorunenTurkiyeIlleri = useMemo(
|
||||||
|
() =>
|
||||||
|
ilAramaNorm
|
||||||
|
? turkiyeIlleri.filter((i) =>
|
||||||
|
normalizeIlAdi(i.il).includes(ilAramaNorm),
|
||||||
|
)
|
||||||
|
: turkiyeIlleri,
|
||||||
|
[turkiyeIlleri, ilAramaNorm],
|
||||||
|
);
|
||||||
|
const gorunenDisIller = useMemo(
|
||||||
|
() =>
|
||||||
|
ilAramaNorm
|
||||||
|
? disIller.filter((i) => normalizeIlAdi(i.il).includes(ilAramaNorm))
|
||||||
|
: disIller,
|
||||||
|
[disIller, ilAramaNorm],
|
||||||
|
);
|
||||||
const haritaIlleri = useMemo(
|
const haritaIlleri = useMemo(
|
||||||
() => canliFacetler.iller.filter((i) => HARITA_ILLERI.has(i.il)),
|
() => canliFacetler.iller.filter((i) => HARITA_ILLERI.has(i.il)),
|
||||||
[canliFacetler.iller],
|
[canliFacetler.iller],
|
||||||
@@ -256,26 +289,85 @@ export function SihirbazAdimlar({
|
|||||||
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-2 transition-colors focus-within:border-slate-300">
|
||||||
|
<Search className="size-4 shrink-0 text-slate-400" aria-hidden />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={ilArama}
|
||||||
|
onChange={(e) => setIlArama(e.target.value)}
|
||||||
|
placeholder="İl ara..."
|
||||||
|
aria-label="İl ara"
|
||||||
|
autoComplete="off"
|
||||||
|
className="min-w-0 flex-1 bg-transparent text-sm text-slate-950 outline-none placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
{ilArama ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIlArama("")}
|
||||||
|
aria-label="Aramayı temizle"
|
||||||
|
className="flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" aria-hidden />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
||||||
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
||||||
süzülmüş, program sayısına göre sıralı listelenir. */}
|
süzülmüş, program sayısına göre sıralı listelenir. Penceresinde
|
||||||
|
programı olan TÜM iller görünür (kırpma yok); arama yalnızca
|
||||||
|
görünümü daraltır, seçimleri etkilemez. */}
|
||||||
<div
|
<div
|
||||||
className={`mt-4 flex flex-wrap gap-2 ${
|
className={`mt-3 flex max-h-56 flex-wrap gap-1.5 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
||||||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{siraliIller.map((i) => (
|
{gorunenTurkiyeIlleri.map((i) => (
|
||||||
<Cip
|
<Cip
|
||||||
key={i.il}
|
key={i.il}
|
||||||
|
kucuk
|
||||||
secili={iller.includes(i.il)}
|
secili={iller.includes(i.il)}
|
||||||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||||
>
|
>
|
||||||
{trBaslikDuzeni(i.il)}
|
{trBaslikDuzeni(i.il)}
|
||||||
<span className="ml-1.5 text-xs opacity-70">{i.adet}</span>
|
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ilAramaNorm &&
|
||||||
|
gorunenTurkiyeIlleri.length === 0 &&
|
||||||
|
gorunenDisIller.length === 0 ? (
|
||||||
|
<p className="mt-3 text-sm text-slate-500">
|
||||||
|
“{ilArama.trim()}” ile eşleşen il yok.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{gorunenDisIller.length > 0 ? (
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
||||||
|
KKTC & Yurt Dışı
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className={`flex flex-wrap gap-1.5 ${
|
||||||
|
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{gorunenDisIller.map((i) => (
|
||||||
|
<Cip
|
||||||
|
key={i.il}
|
||||||
|
kucuk
|
||||||
|
secili={iller.includes(i.il)}
|
||||||
|
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||||
|
>
|
||||||
|
{trBaslikDuzeni(i.il)}
|
||||||
|
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
||||||
|
</Cip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{iller.length > 0 ? (
|
{iller.length > 0 ? (
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-slate-400">
|
<span className="text-xs font-semibold text-slate-400">
|
||||||
@@ -359,6 +451,10 @@ export function SihirbazAdimlar({
|
|||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-slate-500">
|
||||||
|
Öncelikler yapay zekânın gerekçelerine ve program seçimine yön
|
||||||
|
verir; listeyi daraltmaz.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TransitionPanel>
|
</TransitionPanel>
|
||||||
|
|||||||
@@ -126,16 +126,29 @@ export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null {
|
|||||||
return { sira, tur: p.tur, secimler };
|
return { sira, tur: p.tur, secimler };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu kullanır. */
|
/**
|
||||||
|
* Profildeki sıra/tur ile sonuç sayfası URL'i — tüm uygulama aynı sorguyu
|
||||||
|
* kullanır. Profilde sihirbaz seçimleri varsa il + üniversite tipi de URL'e
|
||||||
|
* yazılır: /sonuc'un ham tablosu SSR'da bu paramlarla filtrelenir (tek
|
||||||
|
* doğruluk kaynağı URL; localStorage profili yalnızca URL'i üretir).
|
||||||
|
*/
|
||||||
export function sonucHref(
|
export function sonucHref(
|
||||||
profil: Pick<TercihProfili, "sira" | "tur">,
|
profil: Pick<TercihProfili, "sira" | "tur"> & {
|
||||||
|
secimler?: SihirbazSecimleri;
|
||||||
|
},
|
||||||
ekstra?: Record<string, string>,
|
ekstra?: Record<string, string>,
|
||||||
): string {
|
): string {
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
sira: String(profil.sira),
|
sira: String(profil.sira),
|
||||||
tur: profil.tur,
|
tur: profil.tur,
|
||||||
...ekstra,
|
|
||||||
});
|
});
|
||||||
|
if (profil.secimler) {
|
||||||
|
for (const il of profil.secimler.iller) q.append("il", il);
|
||||||
|
if (profil.secimler.universiteTipi !== "farketmez") {
|
||||||
|
q.set("tip", profil.secimler.universiteTipi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(ekstra ?? {})) q.set(k, v);
|
||||||
return `/sonuc?${q}`;
|
return `/sonuc?${q}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,8 +154,9 @@ export const reports = sqliteTable("reports", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları.
|
// Anonim /sonuc tadımlığı: batch üretilmiş tek tercih satırları.
|
||||||
// Anahtar: sıra kovası × puan türü × kategori ("genel" = kategorisiz fallback).
|
// Anahtar: sıra kovası × puan türü × kategori × üniversite tipi
|
||||||
// Anonim istek LLM'e asla gitmez; bu tablo salt-okunur servis edilir.
|
// ("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;
|
// 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.
|
// prod şeması açılışta yalnızca drizzle/ altındaki migration'larla güncellenir.
|
||||||
export const tadimlikHavuzu = sqliteTable(
|
export const tadimlikHavuzu = sqliteTable(
|
||||||
@@ -165,6 +166,11 @@ export const tadimlikHavuzu = sqliteTable(
|
|||||||
kovaSlug: text("kova_slug").notNull(),
|
kovaSlug: text("kova_slug").notNull(),
|
||||||
tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(),
|
tur: text("tur", { enum: ["say", "ea", "soz", "dil", "tyt"] }).notNull(),
|
||||||
kategoriSlug: text("kategori_slug").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(),
|
programId: text("program_id").notNull(),
|
||||||
dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(),
|
dilim: text("dilim", { enum: ["hayal", "dengeli", "garanti"] }).notNull(),
|
||||||
gerekce: text("gerekce").notNull(),
|
gerekce: text("gerekce").notNull(),
|
||||||
@@ -178,7 +184,7 @@ export const tadimlikHavuzu = sqliteTable(
|
|||||||
uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(),
|
uretimAt: integer("uretim_at", { mode: "timestamp" }).notNull(),
|
||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug),
|
uniqueIndex("tadimlik_anahtar").on(t.kovaSlug, t.tur, t.kategoriSlug, t.tip),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -196,31 +196,38 @@ export function rankWindowFacets(
|
|||||||
|
|
||||||
const satirlar = db
|
const satirlar = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT isim, il, unitur FROM programs
|
`SELECT isim, fakulte, il, unitur FROM programs
|
||||||
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
|
WHERE tur = ? AND onlisans = ? AND ${EFEKTIF_SIRA} IS NOT NULL
|
||||||
AND ${EFEKTIF_SIRA} >= ?`
|
AND ${EFEKTIF_SIRA} >= ?`
|
||||||
)
|
)
|
||||||
.all(tur, onlisans, altSinir) as {
|
.all(tur, onlisans, altSinir) as {
|
||||||
isim: string;
|
isim: string;
|
||||||
|
fakulte: string | null;
|
||||||
il: string | null;
|
il: string | null;
|
||||||
unitur: string | null;
|
unitur: string | null;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
// isim → kategori eşlemesi pahalı (12 regex); tekrar eden isimler için önbellek
|
// isim+fakülte → kategori eşlemesi pahalı (regexler); tekrar edenler için önbellek
|
||||||
const kategoriCache = new Map<string, string[]>();
|
const kategoriCache = new Map<string, string[]>();
|
||||||
const kategorileriGetir = (isim: string): string[] => {
|
const kategorileriGetir = (
|
||||||
let k = kategoriCache.get(isim);
|
isim: string,
|
||||||
|
fakulte: string | null
|
||||||
|
): string[] => {
|
||||||
|
const anahtar = `${isim}|${fakulte ?? ""}`;
|
||||||
|
let k = kategoriCache.get(anahtar);
|
||||||
if (!k) {
|
if (!k) {
|
||||||
k = kategoriBul(isim);
|
k = kategoriBul(isim, fakulte);
|
||||||
kategoriCache.set(isim, k);
|
kategoriCache.set(anahtar, k);
|
||||||
}
|
}
|
||||||
return k;
|
return k;
|
||||||
};
|
};
|
||||||
|
|
||||||
const seciliKategoriler = secim.kategoriler ?? [];
|
const seciliKategoriler = secim.kategoriler ?? [];
|
||||||
const kategoriUyar = (isim: string) =>
|
const kategoriUyar = (isim: string, fakulte: string | null) =>
|
||||||
seciliKategoriler.length === 0 ||
|
seciliKategoriler.length === 0 ||
|
||||||
kategorileriGetir(isim).some((k) => seciliKategoriler.includes(k));
|
kategorileriGetir(isim, fakulte).some((k) =>
|
||||||
|
seciliKategoriler.includes(k)
|
||||||
|
);
|
||||||
const seciliIller = new Set(secim.iller ?? []);
|
const seciliIller = new Set(secim.iller ?? []);
|
||||||
const ilUyar = (il: string | null) =>
|
const ilUyar = (il: string | null) =>
|
||||||
seciliIller.size === 0 || (il != null && seciliIller.has(il));
|
seciliIller.size === 0 || (il != null && seciliIller.has(il));
|
||||||
@@ -230,12 +237,12 @@ export function rankWindowFacets(
|
|||||||
let devlet = 0;
|
let devlet = 0;
|
||||||
let vakif = 0;
|
let vakif = 0;
|
||||||
|
|
||||||
for (const { isim, il, unitur } of satirlar) {
|
for (const { isim, fakulte, il, unitur } of satirlar) {
|
||||||
// 1. adım: kategoriler yalnızca pencereye bağlı (filtre uygulanmaz)
|
// 1. adım: kategoriler yalnızca pencereye bağlı (filtre uygulanmaz)
|
||||||
for (const k of kategorileriGetir(isim)) {
|
for (const k of kategorileriGetir(isim, fakulte)) {
|
||||||
kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + 1);
|
kategoriSay.set(k, (kategoriSay.get(k) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
if (!kategoriUyar(isim)) continue;
|
if (!kategoriUyar(isim, fakulte)) continue;
|
||||||
// 2. adım: iller seçili kategorilerle süzülür
|
// 2. adım: iller seçili kategorilerle süzülür
|
||||||
if (il) ilSay.set(il, (ilSay.get(il) ?? 0) + 1);
|
if (il) ilSay.set(il, (ilSay.get(il) ?? 0) + 1);
|
||||||
if (!ilUyar(il)) continue;
|
if (!ilUyar(il)) continue;
|
||||||
@@ -251,10 +258,11 @@ export function rankWindowFacets(
|
|||||||
.filter((k) => k.adet >= 3) // gürültüyü ele
|
.filter((k) => k.adet >= 3) // gürültüyü ele
|
||||||
.sort((a, b) => b.adet - a.adet);
|
.sort((a, b) => b.adet - a.adet);
|
||||||
|
|
||||||
|
// Penceredeki programı olan TÜM iller döner (81 il + KKTC/yurtdışı);
|
||||||
|
// sıralama/gruplama UI'da yapılır. Kırpma yok: kullanıcı her ili görebilmeli.
|
||||||
const iller = [...ilSay.entries()]
|
const iller = [...ilSay.entries()]
|
||||||
.map(([il, adet]) => ({ il, adet }))
|
.map(([il, adet]) => ({ il, adet }))
|
||||||
.sort((a, b) => b.adet - a.adet)
|
.sort((a, b) => b.adet - a.adet);
|
||||||
.slice(0, 15);
|
|
||||||
|
|
||||||
const uniturler: { grup: UniturGrubu; adet: number }[] = [];
|
const uniturler: { grup: UniturGrubu; adet: number }[] = [];
|
||||||
if (devlet > 0) uniturler.push({ grup: "devlet", adet: devlet });
|
if (devlet > 0) uniturler.push({ grup: "devlet", adet: devlet });
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
|
|||||||
{
|
{
|
||||||
kategori: "Mühendislik & Teknoloji",
|
kategori: "Mühendislik & Teknoloji",
|
||||||
kural:
|
kural:
|
||||||
/mühendis|yazılım|bilgisayar|bilişim|elektrik|elektronik|mekatronik|makine|makina|otomotiv|inşaat|endüstri|metalurji|malzeme|maden|jeoloji|jeofizik|petrol|enerji|robot|yapay zeka|yapay zekâ|siber|ağ teknolojileri|kontrol|otomasyon|uçak|havacılık|uzay|gemi|deniz ulaştırma|teknoloji|teknolojisi|programcı|elektrik-elektronik|nükleer|tekstil müh/,
|
/mühendis|yazılım|bilgisayar|bilişim|elektrik|elektronik|mekatronik|makine|makina|otomotiv|inşaat|endüstri|metalurji|malzeme|maden|jeoloji|jeofizik|petrol|enerji|robot|yapay zeka|yapay zekâ|siber|ağ teknolojileri|kontrol|otomasyon|uçak|havacılık|uzay|gemi|deniz ulaştırma|teknoloji|teknolojisi|bilgisayar programcı|elektrik-elektronik|nükleer|tekstil müh/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Sağlık & Tıp",
|
kategori: "Sağlık & Tıp",
|
||||||
@@ -35,13 +35,15 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Hukuk & Siyasal",
|
kategori: "Hukuk & Siyasal",
|
||||||
|
// "güvenlik" bilinçli olarak dar: jenerik hali Siber/Sosyal/Bilgi
|
||||||
|
// Güvenliği'ni yanlışlıkla bu kategoriye alıyordu.
|
||||||
kural:
|
kural:
|
||||||
/hukuk|adalet|siyaset|kamu yönetim|uluslararası ilişki|siyasal|güvenlik|ceza infaz|milletlerarası|diplomasi/,
|
/hukuk|adalet|siyaset|kamu yönetim|uluslararası ilişki|siyasal|özel güvenlik|ceza infaz|milletlerarası|diplomasi/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "İktisat & İşletme",
|
kategori: "İktisat & İşletme",
|
||||||
kural:
|
kural:
|
||||||
/işletme|iktisat|ekonomi|maliye|muhasebe|finans|bankacılık|sigortacılık|dış ticaret|lojistik|pazarlama|yönetim bilişim|insan kaynak|çalışma ekonomisi|uluslararası ticaret|bankacılık ve sigortacılık|halkla ilişkiler|reklamcılık|emlak|menkul|gümrük|büro yönetim|yönetici asistan/,
|
/işletme|iktisat|iktisadi|ekonomi|maliye|muhasebe|finans|bankacılık|sigortacılık|sosyal güvenlik|dış ticaret|elektronik ticaret|lojistik|pazarlama|yönetim bilişim|havacılık yönetimi|enerji yönetimi|bilgi yönetimi|insan kaynak|çalışma ekonomisi|uluslararası ticaret|bankacılık ve sigortacılık|halkla ilişkiler|reklamcılık|emlak|menkul|gümrük|büro yönetim|yönetici asistan/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Eğitim & Öğretmenlik",
|
kategori: "Eğitim & Öğretmenlik",
|
||||||
@@ -55,13 +57,15 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Fen & Temel Bilimler",
|
kategori: "Fen & Temel Bilimler",
|
||||||
|
// Jenerik "bilim" yok: "Siyaset Bilimi" / "İletişim Bilimleri" /
|
||||||
|
// "Dilbilimi" gibi programları yanlışlıkla Fen yapıyordu.
|
||||||
kural:
|
kural:
|
||||||
/matematik|fizik|kimya|biyoloji|istatistik|moleküler|genetik|biyoteknoloji|astronomi|aktüerya|fen bilim|bilim/,
|
/matematik|fizik|kimya|biyoloji|istatistik|moleküler|genetik|biyoteknoloji|\bastronomi|aktüerya|fen bilim|veri bilim|adli bilim/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Sosyal & Beşeri Bilimler",
|
kategori: "Sosyal & Beşeri Bilimler",
|
||||||
kural:
|
kural:
|
||||||
/sosyoloji|tarih|coğrafya|felsefe|edebiyat|dil ve edebiyat|filoloji|arkeoloji|antropoloji|ilahiyat|din kültürü|dilbilim|sosyal hizmet|mütercim|çeviribilim|tercümanlık|türk dili|dünya dilleri|sanat tarihi|halkbilim|dinî/,
|
/sosyoloji|tarih|coğrafya|felsefe|edebiyat|dil ve edebiyat|filoloji|arkeoloji|antropoloji|ilahiyat|din kültürü|dilbilim|sosyal hizmet|sosyal bilim|islam|mütercim|çeviribilim|tercümanlık|türk dili|dünya dilleri|sanat tarihi|halkbilim|dinî/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Sanat, Medya & İletişim",
|
kategori: "Sanat, Medya & İletişim",
|
||||||
@@ -79,24 +83,108 @@ const KATEGORI_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
kategori: "Tarım & Doğa",
|
kategori: "Tarım & Doğa",
|
||||||
|
// "çevre" bağlamlı: jenerik hali "İç Mimarlık ve Çevre Tasarımı"nı
|
||||||
|
// yanlışlıkla Tarım'a alıyordu.
|
||||||
kural:
|
kural:
|
||||||
/ziraat|tarım|tarla|bahçe|bitki|hayvan|su ürünleri|gıda|orman|çevre|balıkçılık|peyzaj|tohum|toprak|zootekni/,
|
/ziraat|tarım|tarla|bahçe|bitki|hayvan|su ürünleri|gıda|orman|çevre müh|çevre sağlığı|çevre koruma|çevre temizli|çevresel|balıkçılık|peyzaj|tohum|toprak|zootekni/,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Verilen program adının girdiği tüm kategorileri döner (0..n). */
|
// YÖK Atlas'ın kendi alan sinyali: programın bağlı olduğu fakülte adı.
|
||||||
export function kategoriBul(isim: string): Kategori[] {
|
// Lisansta güçlü ("Mühendislik Fakültesi", "Hukuk Fakültesi"); önlisansta
|
||||||
const lower = isim.toLocaleLowerCase("tr-TR");
|
// jenerik ("... Meslek Yüksekokulu") olduğundan hiçbir kurala eşleşmez ve
|
||||||
|
// isim kuralları tek başına devrede kalır. Bir fakülte birden çok kategoriye
|
||||||
|
// eşlenebilir (ör. Fen-Edebiyat → Fen + Sosyal); program bazında ayrım
|
||||||
|
// isim∩fakülte kesişimiyle yapılır (bkz. kategoriBul).
|
||||||
|
const FAKULTE_KURALLARI: { kategori: Kategori; kural: RegExp }[] = [
|
||||||
|
{
|
||||||
|
kategori: "Mühendislik & Teknoloji",
|
||||||
|
kural: /mühendis|teknoloji|bilişim|havacılık ve uzay|denizcilik|uygulamalı bilimler/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Sağlık & Tıp",
|
||||||
|
kural: /tıp|sağlık|diş|eczacılık|hemşirelik|veteriner|spor bilim/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Hukuk & Siyasal",
|
||||||
|
kural: /hukuk|siyasal|iktisadi|idari|güvenlik/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "İktisat & İşletme",
|
||||||
|
kural: /iktisadi|işletme|yönetim|ticaret|bankacılık|uygulamalı bilimler|finans/,
|
||||||
|
},
|
||||||
|
{ kategori: "Eğitim & Öğretmenlik", kural: /eğitim/ },
|
||||||
|
{
|
||||||
|
kategori: "Mimarlık & Tasarım",
|
||||||
|
kural: /mimarlık|tasarım|güzel sanatlar/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Fen & Temel Bilimler",
|
||||||
|
kural: /\bfen\b|fen-edebiyat|doğa bilimleri|temel bilimler/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Sosyal & Beşeri Bilimler",
|
||||||
|
kural:
|
||||||
|
/edebiyat|fen-edebiyat|insan ve toplum|sosyal bilim|ilahiyat|islami|beşeri/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Sanat, Medya & İletişim",
|
||||||
|
kural: /iletişim|güzel sanatlar|sanat|konservatuvar|müzik/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kategori: "Psikoloji & Danışmanlık",
|
||||||
|
kural: /insan ve toplum|edebiyat|fen-edebiyat|sosyal bilim|eğitim/,
|
||||||
|
},
|
||||||
|
{ kategori: "Turizm & Gastronomi", kural: /turizm|gastronomi/ },
|
||||||
|
{
|
||||||
|
kategori: "Tarım & Doğa",
|
||||||
|
kural: /ziraat|orman|su ürünleri|tarım|doğa bilimleri|veteriner/,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function eslesenKategoriler(
|
||||||
|
kurallar: { kategori: Kategori; kural: RegExp }[],
|
||||||
|
metin: string,
|
||||||
|
): Kategori[] {
|
||||||
|
const lower = metin.toLocaleLowerCase("tr-TR");
|
||||||
const sonuc: Kategori[] = [];
|
const sonuc: Kategori[] = [];
|
||||||
for (const { kategori, kural } of KATEGORI_KURALLARI) {
|
for (const { kategori, kural } of kurallar) {
|
||||||
if (kural.test(lower)) sonuc.push(kategori);
|
if (kural.test(lower)) sonuc.push(kategori);
|
||||||
}
|
}
|
||||||
return sonuc;
|
return sonuc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verilen programın girdiği tüm kategorileri döner (0..n).
|
||||||
|
*
|
||||||
|
* Fakülte verilirse isim∩fakülte kesişimi alınır: "Siber Güvenlik
|
||||||
|
* Mühendisliği" isimden {Mühendislik, Hukuk} çıkarken Mühendislik
|
||||||
|
* Fakültesi'nde olduğu için Hukuk elenir. Kesişim boşsa ya da fakülte
|
||||||
|
* hiçbir kurala eşleşmiyorsa (jenerik MYO adları) isim kuralları geçerlidir;
|
||||||
|
* fakültesiz çağrı bugüne kadarki davranışla birebir aynıdır.
|
||||||
|
*/
|
||||||
|
export function kategoriBul(
|
||||||
|
isim: string,
|
||||||
|
fakulte?: string | null,
|
||||||
|
): Kategori[] {
|
||||||
|
const isimKats = eslesenKategoriler(KATEGORI_KURALLARI, isim);
|
||||||
|
if (fakulte && isimKats.length > 0) {
|
||||||
|
const fakulteKats = eslesenKategoriler(FAKULTE_KURALLARI, fakulte);
|
||||||
|
if (fakulteKats.length > 0) {
|
||||||
|
const kesisim = isimKats.filter((k) => fakulteKats.includes(k));
|
||||||
|
if (kesisim.length > 0) return kesisim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isimKats;
|
||||||
|
}
|
||||||
|
|
||||||
/** Bir programın seçili kategorilerden en az biriyle eşleşip eşleşmediği. */
|
/** Bir programın seçili kategorilerden en az biriyle eşleşip eşleşmediği. */
|
||||||
export function kategoriEslesir(isim: string, secililer: string[]): boolean {
|
export function kategoriEslesir(
|
||||||
|
isim: string,
|
||||||
|
secililer: string[],
|
||||||
|
fakulte?: string | null,
|
||||||
|
): boolean {
|
||||||
if (secililer.length === 0) return true;
|
if (secililer.length === 0) return true;
|
||||||
const kats = kategoriBul(isim);
|
const kats = kategoriBul(isim, fakulte);
|
||||||
return kats.some((k) => secililer.includes(k));
|
return kats.some((k) => secililer.includes(k));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function havuzOlustur(params: RaporParams): HavuzSonuc {
|
|||||||
|
|
||||||
const kategoriFiltre = (havuz: AdayProgram[]) =>
|
const kategoriFiltre = (havuz: AdayProgram[]) =>
|
||||||
kategoriler.length > 0
|
kategoriler.length > 0
|
||||||
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler))
|
? havuz.filter((p) => kategoriEslesir(p.isim, kategoriler, p.fakulte))
|
||||||
: havuz;
|
: havuz;
|
||||||
|
|
||||||
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
|
// 1. Tam filtre: iller + üniversite tipi (SQL), kategori (JS)
|
||||||
|
|||||||
@@ -46,15 +46,21 @@ export interface TadimlikSatir {
|
|||||||
program: RaporSonuc["programlar"][string];
|
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
|
* Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye
|
||||||
* satır yoksa "genel"e düşer; o da yoksa null — çağıran taraf bileşeni hiç
|
* özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf
|
||||||
* render etmez (boş/generic tadımlık gösterilmez, PRD kuralı).
|
* 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(
|
export async function tadimlikSec(
|
||||||
sira: number,
|
sira: number,
|
||||||
tur: PuanTuruKey,
|
tur: PuanTuruKey,
|
||||||
kategoriSlug?: string,
|
kategoriSlug?: string,
|
||||||
|
tip: TadimlikTip = "genel",
|
||||||
): Promise<TadimlikSatir | null> {
|
): Promise<TadimlikSatir | null> {
|
||||||
const kova = kovaBul(sira);
|
const kova = kovaBul(sira);
|
||||||
const sluglar = [
|
const sluglar = [
|
||||||
@@ -66,6 +72,7 @@ export async function tadimlikSec(
|
|||||||
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
|
eq(schema.tadimlikHavuzu.kovaSlug, kova.slug),
|
||||||
eq(schema.tadimlikHavuzu.tur, tur),
|
eq(schema.tadimlikHavuzu.tur, tur),
|
||||||
eq(schema.tadimlikHavuzu.kategoriSlug, slug),
|
eq(schema.tadimlikHavuzu.kategoriSlug, slug),
|
||||||
|
eq(schema.tadimlikHavuzu.tip, tip),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (satir) {
|
if (satir) {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export function tercihDegerlendir(
|
|||||||
const olumlu: string[] = [];
|
const olumlu: string[] = [];
|
||||||
const celiskiler: string[] = [];
|
const celiskiler: string[] = [];
|
||||||
|
|
||||||
|
// Manuel liste satırında fakülte taşınmıyor (ProgramListeSatiri bilinçli
|
||||||
|
// omit ediyor); eşleşme yalnızca isim kurallarıyla yapılır.
|
||||||
if (kategoriEslesir(program.isim, profil.secimler.kategoriler)) {
|
if (kategoriEslesir(program.isim, profil.secimler.kategoriler)) {
|
||||||
olumlu.push("İlgi alanlarınla uyumlu");
|
olumlu.push("İlgi alanlarınla uyumlu");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user