init
This commit is contained in:
commit
a3a13e408a
33
.eslintrc.js
Normal file
33
.eslintrc.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
es6: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"google",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
project: ["tsconfig.json", "tsconfig.dev.json"],
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
"/lib/**/*", // Ignore built files.
|
||||||
|
"/generated/**/*", // Ignore generated files.
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"import",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"quotes": ["error", "double"],
|
||||||
|
"import/no-unresolved": 0,
|
||||||
|
"indent": ["error", 2],
|
||||||
|
},
|
||||||
|
};
|
||||||
7
.firebaserc
Normal file
7
.firebaserc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "mmday-7900f"
|
||||||
|
},
|
||||||
|
"targets": {},
|
||||||
|
"etags": {}
|
||||||
|
}
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Compiled JavaScript files
|
||||||
|
lib/**/*.js
|
||||||
|
lib/**/*.js.map
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Node.js dependency directory
|
||||||
|
node_modules/
|
||||||
|
*.local
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# 쿼리 파일을 포함한 무시된 디폴트 폴더
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="ASK" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="customHeaders">
|
||||||
|
<set>
|
||||||
|
<option value=""name"" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="uvloop" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8Inspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N803" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/mmday-firebase.iml
generated
Normal file
8
.idea/mmday-firebase.iml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/mmday-firebase.iml" filepath="$PROJECT_DIR$/.idea/mmday-firebase.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
database.rules.json
Normal file
6
database.rules.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
".read": "auth != null",
|
||||||
|
".write": "auth != null"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
dist/index.d.ts
vendored
Normal file
1
dist/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export declare const helloWorld: import("firebase-functions/v2/https").HttpsFunction;
|
||||||
10
dist/index.js
vendored
Normal file
10
dist/index.js
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.helloWorld = void 0;
|
||||||
|
const app_1 = require("firebase-admin/app");
|
||||||
|
const https_1 = require("firebase-functions/v2/https");
|
||||||
|
(0, app_1.initializeApp)();
|
||||||
|
exports.helloWorld = (0, https_1.onRequest)((req, res) => {
|
||||||
|
res.json({ message: "Hello from mmday!" });
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,4CAAmD;AACnD,uDAAwD;AAExD,IAAA,mBAAa,GAAE,CAAC;AAEH,QAAA,UAAU,GAAG,IAAA,iBAAS,EAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC/C,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;AAC7C,CAAC,CAAC,CAAC"}
|
||||||
48
firebase.json
Normal file
48
firebase.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"rules": "y"
|
||||||
|
},
|
||||||
|
"firestore": {
|
||||||
|
"database": "(default)",
|
||||||
|
"location": "asia-northeast3",
|
||||||
|
"rules": "firestore.rules",
|
||||||
|
"indexes": "firestore.indexes.json"
|
||||||
|
},
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"codebase": "default",
|
||||||
|
"source": ".",
|
||||||
|
"runtime": "nodejs22",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git",
|
||||||
|
"firebase-debug.log",
|
||||||
|
"firebase-debug.*.log",
|
||||||
|
"*.local"
|
||||||
|
],
|
||||||
|
"predeploy": [
|
||||||
|
"npm --prefix \"$RESOURCE_DIR\" run lint",
|
||||||
|
"npm --prefix \"$RESOURCE_DIR\" run build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hosting": {
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"rules": "storage.rules"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"providers": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
firestore.indexes.json
Normal file
4
firestore.indexes.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"indexes": [],
|
||||||
|
"fieldOverrides": []
|
||||||
|
}
|
||||||
19
firestore.rules
Normal file
19
firestore.rules
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
|
||||||
|
// This rule allows anyone with your Firestore database reference to view, edit,
|
||||||
|
// and delete all data in your Firestore database. It is useful for getting
|
||||||
|
// started, but it is configured to expire after 30 days because it
|
||||||
|
// leaves your app open to attackers. At that time, all client
|
||||||
|
// requests to your Firestore database will be denied.
|
||||||
|
//
|
||||||
|
// Make sure to write security rules for your app before that time, or else
|
||||||
|
// all client requests to your Firestore database will be denied until you Update
|
||||||
|
// your rules
|
||||||
|
match /{document=**} {
|
||||||
|
allow read, write: if request.time < timestamp.date(2026, 5, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10146
package-lock.json
generated
Normal file
10146
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "functions",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint --ext .js,.ts .",
|
||||||
|
"build": "tsc",
|
||||||
|
"build:watch": "tsc --watch",
|
||||||
|
"serve": "npm run build && firebase emulators:start --only functions",
|
||||||
|
"shell": "npm run build && firebase functions:shell",
|
||||||
|
"start": "npm run shell",
|
||||||
|
"deploy": "firebase deploy --only functions",
|
||||||
|
"logs": "firebase functions:log"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "24"
|
||||||
|
},
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"firebase-admin": "^13.6.0",
|
||||||
|
"firebase-functions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||||
|
"@typescript-eslint/parser": "^5.12.0",
|
||||||
|
"eslint": "^8.9.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"firebase-functions-test": "^3.4.1",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
89
public/index.html
Normal file
89
public/index.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Welcome to Firebase Hosting</title>
|
||||||
|
|
||||||
|
<!-- update the version number as needed -->
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-app-compat.js"></script>
|
||||||
|
<!-- include only the Firebase features as you need -->
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-auth-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-database-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-firestore-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-functions-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-messaging-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-storage-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-analytics-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-remote-config-compat.js"></script>
|
||||||
|
<script defer src="/__/firebase/12.11.0/firebase-performance-compat.js"></script>
|
||||||
|
<!--
|
||||||
|
initialize the SDK after all desired features are loaded, set useEmulator to false
|
||||||
|
to avoid connecting the SDK to running emulators.
|
||||||
|
-->
|
||||||
|
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
|
||||||
|
|
||||||
|
<style media="screen">
|
||||||
|
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
||||||
|
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
|
||||||
|
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
||||||
|
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
||||||
|
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
||||||
|
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
||||||
|
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
||||||
|
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
||||||
|
body { border-top: 16px solid #ffa100; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="message">
|
||||||
|
<h2>Welcome</h2>
|
||||||
|
<h1>Firebase Hosting Setup Complete</h1>
|
||||||
|
<p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
|
||||||
|
<a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
|
||||||
|
</div>
|
||||||
|
<p id="load">Firebase SDK Loading…</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const loadEl = document.querySelector('#load');
|
||||||
|
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
|
// // The Firebase SDK is initialized and available here!
|
||||||
|
//
|
||||||
|
// firebase.auth().onAuthStateChanged(user => { });
|
||||||
|
// firebase.database().ref('/path/to/ref').on('value', snapshot => { });
|
||||||
|
// firebase.firestore().doc('/foo/bar').get().then(() => { });
|
||||||
|
// firebase.functions().httpsCallable('yourFunction')().then(() => { });
|
||||||
|
// firebase.messaging().requestPermission().then(() => { });
|
||||||
|
// firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
|
||||||
|
// firebase.analytics(); // call to activate
|
||||||
|
// firebase.analytics().logEvent('tutorial_completed');
|
||||||
|
// firebase.performance(); // call to activate
|
||||||
|
//
|
||||||
|
// // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
|
|
||||||
|
try {
|
||||||
|
let app = firebase.app();
|
||||||
|
let features = [
|
||||||
|
'auth',
|
||||||
|
'database',
|
||||||
|
'firestore',
|
||||||
|
'functions',
|
||||||
|
'messaging',
|
||||||
|
'storage',
|
||||||
|
'analytics',
|
||||||
|
'remoteConfig',
|
||||||
|
'performance',
|
||||||
|
].filter(feature => typeof app[feature] === 'function');
|
||||||
|
loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
src/index.ts
Normal file
32
src/index.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Import function triggers from their respective submodules:
|
||||||
|
*
|
||||||
|
* import {onCall} from "firebase-functions/v2/https";
|
||||||
|
* import {onDocumentWritten} from "firebase-functions/v2/firestore";
|
||||||
|
*
|
||||||
|
* See a full list of supported triggers at https://firebase.google.com/docs/functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {setGlobalOptions} from "firebase-functions";
|
||||||
|
import {onRequest} from "firebase-functions/https";
|
||||||
|
import * as logger from "firebase-functions/logger";
|
||||||
|
|
||||||
|
// Start writing functions
|
||||||
|
// https://firebase.google.com/docs/functions/typescript
|
||||||
|
|
||||||
|
// For cost control, you can set the maximum number of containers that can be
|
||||||
|
// running at the same time. This helps mitigate the impact of unexpected
|
||||||
|
// traffic spikes by instead downgrading performance. This limit is a
|
||||||
|
// per-function limit. You can override the limit for each function using the
|
||||||
|
// `maxInstances` option in the function's options, e.g.
|
||||||
|
// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`.
|
||||||
|
// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1
|
||||||
|
// functions should each use functions.runWith({ maxInstances: 10 }) instead.
|
||||||
|
// In the v1 API, each function can only serve one request per container, so
|
||||||
|
// this will be the maximum concurrent request count.
|
||||||
|
setGlobalOptions({ maxInstances: 10 });
|
||||||
|
|
||||||
|
// export const helloWorld = onRequest((request, response) => {
|
||||||
|
// logger.info("Hello logs!", {structuredData: true});
|
||||||
|
// response.send("Hello from Firebase!");
|
||||||
|
// });
|
||||||
96
src/kbo/aspnet-client.ts
Normal file
96
src/kbo/aspnet-client.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { extractHiddenField } from "./html-utils.js";
|
||||||
|
|
||||||
|
export const PREFIX =
|
||||||
|
"ctl00$ctl00$ctl00$cphContents$cphContents$cphContents$";
|
||||||
|
|
||||||
|
export interface PageState {
|
||||||
|
viewState: string;
|
||||||
|
viewStateGenerator: string;
|
||||||
|
eventValidation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostbackRequest {
|
||||||
|
url: string;
|
||||||
|
eventTarget: string;
|
||||||
|
scriptManager: string;
|
||||||
|
formFields: Record<string, string>;
|
||||||
|
state: PageState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostbackResponse {
|
||||||
|
html: string;
|
||||||
|
newState: PageState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/147.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
export async function getInitialPageState(
|
||||||
|
url: string
|
||||||
|
): Promise<{ html: string; state: PageState }> {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { "User-Agent": USER_AGENT },
|
||||||
|
});
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
state: {
|
||||||
|
viewState: extractHiddenField(html, "__VIEWSTATE"),
|
||||||
|
viewStateGenerator: extractHiddenField(html, "__VIEWSTATEGENERATOR"),
|
||||||
|
eventValidation: extractHiddenField(html, "__EVENTVALIDATION"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postback(
|
||||||
|
request: PostbackRequest
|
||||||
|
): Promise<PostbackResponse> {
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
|
||||||
|
formData.set(`${PREFIX}ScriptManager`, request.scriptManager);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(request.formFields)) {
|
||||||
|
formData.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.set("__EVENTTARGET", request.eventTarget);
|
||||||
|
formData.set("__EVENTARGUMENT", "");
|
||||||
|
formData.set("__LASTFOCUS", "");
|
||||||
|
formData.set("__VIEWSTATE", request.state.viewState);
|
||||||
|
formData.set("__VIEWSTATEGENERATOR", request.state.viewStateGenerator);
|
||||||
|
formData.set("__EVENTVALIDATION", request.state.eventValidation);
|
||||||
|
formData.set("__ASYNCPOST", "true");
|
||||||
|
|
||||||
|
const res = await fetch(request.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"X-MicrosoftAjax": "Delta=true",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
"User-Agent": USER_AGENT,
|
||||||
|
Referer: request.url,
|
||||||
|
Origin: new URL(request.url).origin,
|
||||||
|
},
|
||||||
|
body: formData.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
const panelMatch = text.match(
|
||||||
|
/updatePanel\|[^|]*\|([\s\S]*?)\|[0-9]+\|hiddenField\|/
|
||||||
|
);
|
||||||
|
const html = panelMatch?.[1] ?? text;
|
||||||
|
|
||||||
|
const vsMatch = text.match(/__VIEWSTATE\|([^|]*)\|/);
|
||||||
|
const vsgMatch = text.match(/__VIEWSTATEGENERATOR\|([^|]*)\|/);
|
||||||
|
const evMatch = text.match(/__EVENTVALIDATION\|([^|]*)\|/);
|
||||||
|
|
||||||
|
const newState: PageState = {
|
||||||
|
viewState: vsMatch?.[1] ?? request.state.viewState,
|
||||||
|
viewStateGenerator: vsgMatch?.[1] ?? request.state.viewStateGenerator,
|
||||||
|
eventValidation: evMatch?.[1] ?? request.state.eventValidation,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { html, newState };
|
||||||
|
}
|
||||||
29
src/kbo/html-utils.ts
Normal file
29
src/kbo/html-utils.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export function extractHiddenField(html: string, name: string): string {
|
||||||
|
const re = new RegExp(`id="${name}"[^>]*value="([^"]*)"`, "i");
|
||||||
|
const match = html.match(re);
|
||||||
|
if (match) return match[1];
|
||||||
|
const re2 = new RegExp(`name="${name}"[^>]*value="([^"]*)"`, "i");
|
||||||
|
const match2 = html.match(re2);
|
||||||
|
return match2?.[1] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripTags(html: string): string {
|
||||||
|
return html.replace(/<[^>]*>/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeHtmlEntities(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function padCell(val: string, w: number): string {
|
||||||
|
const displayWidth = [...val].reduce(
|
||||||
|
(sum, ch) => sum + (ch.charCodeAt(0) > 0x7f ? 2 : 1),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return val + " ".repeat(Math.max(0, w - displayWidth));
|
||||||
|
}
|
||||||
210
src/kbo/team-rank.ts
Normal file
210
src/kbo/team-rank.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import { stripTags, decodeHtmlEntities } from "./html-utils.js";
|
||||||
|
import {
|
||||||
|
PREFIX,
|
||||||
|
postback,
|
||||||
|
getInitialPageState,
|
||||||
|
type PageState,
|
||||||
|
type PostbackResponse,
|
||||||
|
} from "./aspnet-client.js";
|
||||||
|
|
||||||
|
const PAGE_URL =
|
||||||
|
"https://www.koreabaseball.com/Record/TeamRank/TeamRank.aspx";
|
||||||
|
|
||||||
|
// ── Types ──
|
||||||
|
|
||||||
|
export interface TeamRank {
|
||||||
|
rank: string;
|
||||||
|
team: string;
|
||||||
|
games: string;
|
||||||
|
wins: string;
|
||||||
|
losses: string;
|
||||||
|
draws: string;
|
||||||
|
winRate: string;
|
||||||
|
gamesBehind: string;
|
||||||
|
last10: string;
|
||||||
|
streak: string;
|
||||||
|
home: string;
|
||||||
|
away: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WinLossDraw = [win: number, loss: number, draw: number];
|
||||||
|
|
||||||
|
export interface TeamVsRecord {
|
||||||
|
team: string;
|
||||||
|
headToHead: Record<string, WinLossDraw>;
|
||||||
|
total: WinLossDraw;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamRankResult {
|
||||||
|
year: number;
|
||||||
|
teams: TeamRank[];
|
||||||
|
vsRecords: TeamVsRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Parsing ──
|
||||||
|
|
||||||
|
export function parseWLD(s: string): WinLossDraw {
|
||||||
|
const parts = s.split("-").map(Number);
|
||||||
|
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRankTable(html: string): TeamRank[] {
|
||||||
|
const teams: TeamRank[] = [];
|
||||||
|
|
||||||
|
const trRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||||
|
let trMatch: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((trMatch = trRegex.exec(html)) !== null) {
|
||||||
|
const trContent = trMatch[1];
|
||||||
|
const tds: string[] = [];
|
||||||
|
const tdRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
|
||||||
|
let tdMatch: RegExpExecArray | null;
|
||||||
|
while ((tdMatch = tdRegex.exec(trContent)) !== null) {
|
||||||
|
tds.push(stripTags(decodeHtmlEntities(tdMatch[1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tds.length >= 10) {
|
||||||
|
const teamName = tds[1];
|
||||||
|
if (/^[A-Z가-힣]/.test(teamName)) {
|
||||||
|
teams.push({
|
||||||
|
rank: tds[0],
|
||||||
|
team: tds[1],
|
||||||
|
games: tds[2],
|
||||||
|
wins: tds[3],
|
||||||
|
losses: tds[4],
|
||||||
|
draws: tds[5],
|
||||||
|
winRate: tds[6],
|
||||||
|
gamesBehind: tds[7],
|
||||||
|
last10: tds[8],
|
||||||
|
streak: tds[9],
|
||||||
|
home: tds[10] ?? "",
|
||||||
|
away: tds[11] ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVsTable(html: string): TeamVsRecord[] {
|
||||||
|
const tableMatch = html.match(
|
||||||
|
/<table[^>]*summary="팀간승패표"[^>]*>([\s\S]*?)<\/table>/
|
||||||
|
);
|
||||||
|
if (!tableMatch) return [];
|
||||||
|
|
||||||
|
const tableHtml = tableMatch[1];
|
||||||
|
|
||||||
|
const theadMatch = tableHtml.match(/<thead[^>]*>([\s\S]*?)<\/thead>/);
|
||||||
|
const teamNames: string[] = [];
|
||||||
|
if (theadMatch) {
|
||||||
|
const thRegex = /<th[^>]*>([\s\S]*?)<\/th>/gi;
|
||||||
|
let thMatch: RegExpExecArray | null;
|
||||||
|
while ((thMatch = thRegex.exec(theadMatch[1])) !== null) {
|
||||||
|
const text = stripTags(decodeHtmlEntities(thMatch[1]))
|
||||||
|
.replace(/\(승-패-무\)/g, "")
|
||||||
|
.trim();
|
||||||
|
if (text && text !== "팀명" && text !== "합계") {
|
||||||
|
teamNames.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const records: TeamVsRecord[] = [];
|
||||||
|
const tbodyMatch = tableHtml.match(/<tbody[^>]*>([\s\S]*?)<\/tbody>/);
|
||||||
|
if (!tbodyMatch) return [];
|
||||||
|
|
||||||
|
const trRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
||||||
|
let trMatch: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((trMatch = trRegex.exec(tbodyMatch[1])) !== null) {
|
||||||
|
const cells: string[] = [];
|
||||||
|
const tdRegex = /<td[^>]*>([\s\S]*?)<\/td>/gi;
|
||||||
|
let tdMatch: RegExpExecArray | null;
|
||||||
|
while ((tdMatch = tdRegex.exec(trMatch[1])) !== null) {
|
||||||
|
cells.push(stripTags(decodeHtmlEntities(tdMatch[1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cells.length < teamNames.length + 2) continue;
|
||||||
|
|
||||||
|
const team = cells[0];
|
||||||
|
const headToHead: Record<string, WinLossDraw> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < teamNames.length; i++) {
|
||||||
|
const val = cells[i + 1];
|
||||||
|
if (val === "■" || val === "▲" || team === teamNames[i]) continue;
|
||||||
|
headToHead[teamNames[i]] = parseWLD(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = parseWLD(cells[cells.length - 1]);
|
||||||
|
records.push({ team, headToHead, total });
|
||||||
|
}
|
||||||
|
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page-specific helpers ──
|
||||||
|
|
||||||
|
export function extractCurrentYear(html: string): number {
|
||||||
|
const m = html.match(
|
||||||
|
/id="cphContents_cphContents_cphContents_lblSearchDateTitle">(\d{4})/
|
||||||
|
);
|
||||||
|
return m ? parseInt(m[1], 10) : new Date().getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildYearFormFields(
|
||||||
|
year: number,
|
||||||
|
currentYear: number
|
||||||
|
): Record<string, string> {
|
||||||
|
return {
|
||||||
|
[`${PREFIX}ddlYear`]: String(year),
|
||||||
|
[`${PREFIX}ddlSeries`]: "0",
|
||||||
|
[`${PREFIX}hfSearchYear`]: String(currentYear),
|
||||||
|
[`${PREFIX}hfSearchDate`]: `${currentYear}1231`,
|
||||||
|
[`${PREFIX}hfSearchSeries`]: "0",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fetch ──
|
||||||
|
|
||||||
|
export async function fetchTeamRank(
|
||||||
|
year: number,
|
||||||
|
state: PageState,
|
||||||
|
currentYear: number
|
||||||
|
): Promise<{ result: TeamRankResult; newState: PageState }> {
|
||||||
|
const { html, newState }: PostbackResponse = await postback({
|
||||||
|
url: PAGE_URL,
|
||||||
|
eventTarget: `${PREFIX}ddlYear`,
|
||||||
|
scriptManager: `${PREFIX}udpRecord|${PREFIX}ddlYear`,
|
||||||
|
formFields: buildYearFormFields(year, currentYear),
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: {
|
||||||
|
year,
|
||||||
|
teams: parseRankTable(html),
|
||||||
|
vsRecords: parseVsTable(html),
|
||||||
|
},
|
||||||
|
newState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTeamRankInitial(): Promise<{
|
||||||
|
result: TeamRankResult;
|
||||||
|
state: PageState;
|
||||||
|
currentYear: number;
|
||||||
|
}> {
|
||||||
|
const { html, state } = await getInitialPageState(PAGE_URL);
|
||||||
|
const currentYear = extractCurrentYear(html);
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: {
|
||||||
|
year: currentYear,
|
||||||
|
teams: parseRankTable(html),
|
||||||
|
vsRecords: parseVsTable(html),
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
currentYear,
|
||||||
|
};
|
||||||
|
}
|
||||||
9
storage.rules
Normal file
9
storage.rules
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
service firebase.storage {
|
||||||
|
match /b/{bucket}/o {
|
||||||
|
match /{allPaths=**} {
|
||||||
|
allow read, write: if request.auth != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
tsconfig.dev.json
Normal file
5
tsconfig.dev.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"include": [
|
||||||
|
".eslintrc.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"compileOnSave": true,
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user