first commit
This commit is contained in:
commit
103a69afe7
15 changed files with 1029 additions and 0 deletions
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
**/*.trace
|
||||
**/*.zip
|
||||
**/*.tar.gz
|
||||
**/*.tgz
|
||||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
|
||||
.cache
|
||||
.env
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
@imnyang:registry=https://git.mizuki.guru/api/packages/imnyang/npm/
|
||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Elysia with Bun runtime
|
||||
|
||||
## Getting Started
|
||||
To get started with this template, simply paste this command into your terminal:
|
||||
```bash
|
||||
bun create elysia ./elysia-example
|
||||
```
|
||||
|
||||
## Development
|
||||
To start the development server run:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000/ with your browser to see the result.
|
||||
140
bun.lock
Normal file
140
bun.lock
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "kanade-api",
|
||||
"dependencies": {
|
||||
"@elysiajs/openapi": "^1.4.14",
|
||||
"@elysiajs/swagger": "^1.3.1",
|
||||
"@imnyang/comcigan.ts": "^0.3.1",
|
||||
"elysia": "latest",
|
||||
"lmdb": "^3.5.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
|
||||
|
||||
"@elysiajs/openapi": ["@elysiajs/openapi@1.4.14", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-kWmJWdvP8/LwHwAJXSpz6xFfYUoyUyEPRimEYABuDU1rOnS27Da1u9T2jyU7frOopxKWV/wDfDxMP8z2xdCPJw=="],
|
||||
|
||||
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
|
||||
|
||||
"@harperfast/extended-iterable": ["@harperfast/extended-iterable@1.0.3", "", {}, "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw=="],
|
||||
|
||||
"@imnyang/comcigan.ts": ["@imnyang/comcigan.ts@0.3.1", "https://git.mizuki.guru/api/packages/imnyang/npm/%40imnyang%2Fcomcigan.ts/-/0.3.1/comcigan.ts-0.3.1.tgz", { "dependencies": { "iconv-lite": "^0.6.3", "undici": "^6.23.0" } }, "sha512-mWeAXUOlBrpWBTPLXJ38hhj7TVyzag23ffgkAbrPXzbN2951NC3Fj9GxXYUQDb5pGvvhWYdKaqbMAiog0zPJJg=="],
|
||||
|
||||
"@lmdb/lmdb-darwin-arm64": ["@lmdb/lmdb-darwin-arm64@3.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ob379nnG6FpfVi9WUVupUVsMFa0+jbkelilrBAdJgNlg6dDtXKeTi+pzL+G3f1z3SNdXXWUAL5N8LTp7szXEVw=="],
|
||||
|
||||
"@lmdb/lmdb-darwin-x64": ["@lmdb/lmdb-darwin-x64@3.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fbKZ6gonDCWENiXiRoDC4KdBBXi2rlDr1uYj/SErpIAHuTfnLo0Il1hvmbDLeiCPLaPjbXlvcMiR3vLnrNnWMw=="],
|
||||
|
||||
"@lmdb/lmdb-linux-arm": ["@lmdb/lmdb-linux-arm@3.5.3", "", { "os": "linux", "cpu": "arm" }, "sha512-A80EUIRBiKA+0iMc5DxT2u8msgY+K05Lok133IKb3eJVlJkmJie4+LM0MjNyV+mREnu8UwhlNFupSikPy/eTPw=="],
|
||||
|
||||
"@lmdb/lmdb-linux-arm64": ["@lmdb/lmdb-linux-arm64@3.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VYWkuWS8uQSyszMe5KGVJPD3YSkaXVrUz/6hbg3zkBvhfOTyrIVMN9M3cZjpU4yxVRBmGViZ5kgjoKz7n3sw2w=="],
|
||||
|
||||
"@lmdb/lmdb-linux-x64": ["@lmdb/lmdb-linux-x64@3.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-JAeG8rJaL1klzg+VKyRqp0wPSbuPo1ZuNmO/IBRs8QRrSIPxF9r3r1TcIVVRLaaJmI0+2KOjbFRjtvFWFH6cMQ=="],
|
||||
|
||||
"@lmdb/lmdb-win32-arm64": ["@lmdb/lmdb-win32-arm64@3.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-9QdgjU5VW0MJ3wy94h4fQ+cNkxeJ0KasjvisOhLuvlCep2KSOzr1i65Js3ElHBRQX8N+jVD8/+LWIM7flfGZ4w=="],
|
||||
|
||||
"@lmdb/lmdb-win32-x64": ["@lmdb/lmdb-win32-x64@3.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-0nd13c9ypIDkdsJbHv1PMvk6MZrwHLQP05AXZdxvv8lklxRrZxPK2d8nSo8HLoMXyLt0hA2yQ3fydQaSRrkz0g=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
|
||||
|
||||
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
"file-type": ["file-type@22.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"lmdb": ["lmdb@3.5.3", "", { "dependencies": { "@harperfast/extended-iterable": "^1.0.3", "msgpackr": "^1.11.2", "node-addon-api": "^6.1.0", "node-gyp-build-optional-packages": "5.2.2", "ordered-binary": "^1.5.3", "weak-lru-cache": "^1.2.2" }, "optionalDependencies": { "@lmdb/lmdb-darwin-arm64": "3.5.3", "@lmdb/lmdb-darwin-x64": "3.5.3", "@lmdb/lmdb-linux-arm": "3.5.3", "@lmdb/lmdb-linux-arm64": "3.5.3", "@lmdb/lmdb-linux-x64": "3.5.3", "@lmdb/lmdb-win32-arm64": "3.5.3", "@lmdb/lmdb-win32-x64": "3.5.3" }, "bin": { "download-lmdb-prebuilds": "bin/download-prebuilds.js" } }, "sha512-6A0iRKOv/N62P1vU8qhBr32tHQIY6pQMe8b6zqI4VLELqV8fWHUMdnfNjMR+Kyh7ZeRCo8PDzAnW2g+SJSoP1A=="],
|
||||
|
||||
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"ordered-binary": ["ordered-binary@1.6.1", "", {}, "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"weak-lru-cache": ["weak-lru-cache@1.2.2", "", {}, "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw=="],
|
||||
|
||||
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
}
|
||||
}
|
||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[install.scopes]
|
||||
imnyang = "https://git.mizuki.guru/api/packages/imnyang/npm/"
|
||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "kanade-api",
|
||||
"version": "1.0.50",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/openapi": "^1.4.14",
|
||||
"@elysiajs/swagger": "^1.3.1",
|
||||
"@imnyang/comcigan.ts": "^0.3.1",
|
||||
"elysia": "latest",
|
||||
"lmdb": "^3.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
||||
14
src/index.ts
Normal file
14
src/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Elysia } from "elysia";
|
||||
import { openapi } from '@elysiajs/openapi';
|
||||
|
||||
const app = new Elysia()
|
||||
.use(openapi())
|
||||
.get("/", () => "Hello Elysia")
|
||||
.use(import("./routes/comcigan"))
|
||||
.use(import("./routes/neis"))
|
||||
|
||||
.listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
138
src/routes/comcigan.ts
Normal file
138
src/routes/comcigan.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import Comcigan, { School, Weekday } from '@imnyang/comcigan.ts';
|
||||
import { open } from 'lmdb';
|
||||
|
||||
const comcigan = new Comcigan();
|
||||
type CacheEntry = {
|
||||
expiresAt: number;
|
||||
value: any;
|
||||
};
|
||||
|
||||
const cacheDb = open<CacheEntry>('./.cache/comcigan', {
|
||||
compression: true,
|
||||
});
|
||||
|
||||
function getNextDaily8AM(now = new Date()): number {
|
||||
const next = new Date(now);
|
||||
next.setHours(8, 0, 0, 0);
|
||||
|
||||
if (now >= next) {
|
||||
next.setDate(next.getDate() + 1);
|
||||
}
|
||||
|
||||
return next.getTime();
|
||||
}
|
||||
|
||||
function getCacheKey(route: 'search' | 'timetable', params: Record<string, unknown>): string {
|
||||
const sorted = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
|
||||
return `${route}:${JSON.stringify(sorted)}`;
|
||||
}
|
||||
|
||||
export default new Elysia({ prefix: "/comcigan", tags: ["컴시간"] })
|
||||
.get('/search', async ({ query: { schoolName } }) => {
|
||||
const cacheKey = getCacheKey('search', { schoolName });
|
||||
const cached = cacheDb.get(cacheKey);
|
||||
const nowMs = Date.now();
|
||||
|
||||
if (cached && cached.expiresAt > nowMs) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
cacheDb.removeSync(cacheKey);
|
||||
}
|
||||
|
||||
const searchedSchools: School[] = await comcigan.searchSchools(schoolName);
|
||||
|
||||
const result = searchedSchools
|
||||
.filter((school) => school.code !== 0) // 없으면 추가 검색하세요 제외
|
||||
.map((school) => ({
|
||||
schoolName: school.name,
|
||||
schoolCode: school.code,
|
||||
region: school.region.name,
|
||||
}));
|
||||
|
||||
cacheDb.putSync(cacheKey, {
|
||||
expiresAt: getNextDaily8AM(),
|
||||
value: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
{
|
||||
query: t.Object({
|
||||
schoolName: t.String({ description: '학교 이름' }),
|
||||
}),
|
||||
detail: { summary: '학교 검색' },
|
||||
response: {
|
||||
200: t.Array(
|
||||
t.Object({
|
||||
schoolName: t.String({ description: '학교 이름', default: '선린인터넷고' }),
|
||||
schoolCode: t.Number({ description: '학교 코드', default: 41896 }),
|
||||
region: t.String({ description: '지역', default: '서울' }),
|
||||
}),
|
||||
{ description: '검색된 학교 목록' }
|
||||
),
|
||||
400: t.Object({ message: t.String() }, { description: '에러 메시지' }),
|
||||
},
|
||||
}
|
||||
)
|
||||
.get("/timetable/:school", async ({ params: { school }, query: { grade, classNum, weekday, nextWeek } }) => {
|
||||
const cacheKey = getCacheKey('timetable', {
|
||||
school,
|
||||
grade,
|
||||
classNum,
|
||||
weekday,
|
||||
nextWeek: nextWeek ?? false,
|
||||
});
|
||||
const cached = cacheDb.get(cacheKey);
|
||||
const nowMs = Date.now();
|
||||
|
||||
if (cached && cached.expiresAt > nowMs) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
cacheDb.removeSync(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (weekday && classNum !== undefined) {
|
||||
// 특정 반의 특정 요일
|
||||
result = await comcigan.getTimetable(school, grade, Number(classNum), Number(weekday), nextWeek);
|
||||
} else if (weekday && classNum === undefined) {
|
||||
// 학년 전체의 특정 요일
|
||||
const fullTimetable = await comcigan.getTimetable(school, grade, nextWeek);
|
||||
const weekdayIndex = Number(weekday) - 1; // '1' -> 0, '2' -> 1, etc.
|
||||
result = fullTimetable.map(gradeClasses => gradeClasses.map(classTimetable => classTimetable[weekdayIndex]));
|
||||
} else if (classNum !== undefined) {
|
||||
// 특정 반의 전체 주
|
||||
result = await comcigan.getTimetable(school, grade, classNum, nextWeek);
|
||||
} else {
|
||||
// 학년 전체의 전체 주
|
||||
result = await comcigan.getTimetable(school, grade, nextWeek);
|
||||
}
|
||||
|
||||
cacheDb.putSync(cacheKey, {
|
||||
expiresAt: getNextDaily8AM(),
|
||||
value: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { message: '시간표를 불러오는 중 오류가 발생했습니다.' };
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
grade: t.Number({ description: '학년' }),
|
||||
classNum: t.Number({ description: '반' }),
|
||||
weekday: t.Optional(t.Enum(Weekday, { description: '요일 (1: 월, 2: 화, 3: 수, 4: 목, 5: 금)' })),
|
||||
nextWeek: t.Optional(t.Boolean({default: false, description: '다음 주 시간표를 불러올지 여부'})),
|
||||
}),
|
||||
params: t.Object({
|
||||
school: t.Number({ description: '학교 코드' })
|
||||
}),
|
||||
detail: { summary: '시간표 조회' },
|
||||
})
|
||||
150
src/routes/lib/neis.ts
Normal file
150
src/routes/lib/neis.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { open } from 'lmdb';
|
||||
|
||||
type NeisType =
|
||||
| 'schoolInfo'
|
||||
| 'classInfo'
|
||||
| 'hisTimetable'
|
||||
| 'misTimetable'
|
||||
| 'elsTimetable'
|
||||
| 'spsTimetable'
|
||||
| 'SchoolSchedule'
|
||||
| 'mealServiceDietInfo';
|
||||
|
||||
type CacheEntry = {
|
||||
expiresAt: number;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
const NEIS_FETCH_TIMEOUT_MS = 8000;
|
||||
|
||||
let cacheDb: ReturnType<typeof open<CacheEntry>> | null = null;
|
||||
let cacheDisabled = false;
|
||||
|
||||
function getCacheDb(): ReturnType<typeof open<CacheEntry>> | null {
|
||||
if (cacheDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cacheDb) {
|
||||
return cacheDb;
|
||||
}
|
||||
|
||||
try {
|
||||
cacheDb = open<CacheEntry>({
|
||||
path: './.cache/neis',
|
||||
compression: true,
|
||||
});
|
||||
return cacheDb;
|
||||
} catch (error) {
|
||||
cacheDisabled = true;
|
||||
console.error('[neis-cache] LMDB open failed, fallback to no-cache mode', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const MONTHLY_CACHE_TYPES: NeisType[] = ['schoolInfo', 'classInfo'];
|
||||
|
||||
function getExpiresAt(type: NeisType, now = new Date()): number {
|
||||
if (MONTHLY_CACHE_TYPES.includes(type)) {
|
||||
return new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0).getTime();
|
||||
}
|
||||
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0).getTime();
|
||||
}
|
||||
|
||||
function getCacheKey(type: NeisType, params: Record<string, string>): string {
|
||||
const sorted = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
|
||||
return `${type}:${JSON.stringify(sorted)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - schoolInfo | classInfo | hisTimetable | misTimetable | elsTimetable | spsTimetable | SchoolSchedule | mealServiceDietInfo
|
||||
* @param params - { ATPT_OFCDC_SC_CODE: string, SD_SCHUL_CODE: string }
|
||||
* @returns Promise<any>
|
||||
*/
|
||||
export function neisFetch(
|
||||
type: NeisType,
|
||||
params: Record<string, string>
|
||||
): Promise<any> {
|
||||
const baseUrl = 'https://open.neis.go.kr/hub';
|
||||
const apiKey = process.env.NEIS_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('NEIS_API_KEY is not set');
|
||||
}
|
||||
|
||||
const db = getCacheDb();
|
||||
const cacheKey = getCacheKey(type, params);
|
||||
const cached = db?.get(cacheKey);
|
||||
const nowMs = Date.now();
|
||||
|
||||
if (cached && cached.expiresAt > nowMs) {
|
||||
return Promise.resolve(cached.value);
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
db?.removeSync(cacheKey);
|
||||
}
|
||||
|
||||
const url = new URL(`${baseUrl}/${type}`);
|
||||
url.searchParams.append('KEY', apiKey!);
|
||||
url.searchParams.append('Type', 'json');
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), NEIS_FETCH_TIMEOUT_MS);
|
||||
|
||||
return fetch(url.toString(), { signal: controller.signal })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
const value = data;
|
||||
db?.putSync(cacheKey, {
|
||||
expiresAt: getExpiresAt(type),
|
||||
value,
|
||||
});
|
||||
|
||||
return value;
|
||||
})
|
||||
.finally(() => clearTimeout(timeout));
|
||||
}
|
||||
|
||||
export async function schoolInfoFetch(SD_SCHUL_CODE?: string, SCHUL_NM?: string): Promise<any> {
|
||||
if (!SD_SCHUL_CODE && !SCHUL_NM) {
|
||||
throw new Error('SD_SCHUL_CODE 또는 SCHUL_NM 중 하나는 반드시 제공되어야 합니다');
|
||||
}
|
||||
if (SD_SCHUL_CODE && SCHUL_NM) {
|
||||
throw new Error('SD_SCHUL_CODE와 SCHUL_NM은 함께 사용할 수 없습니다');
|
||||
}
|
||||
let result;
|
||||
|
||||
if (SD_SCHUL_CODE && !SCHUL_NM) {
|
||||
result = await neisFetch('schoolInfo', {
|
||||
SD_SCHUL_CODE: SD_SCHUL_CODE as string,
|
||||
});
|
||||
}
|
||||
|
||||
if (SCHUL_NM && !SD_SCHUL_CODE) {
|
||||
result = await neisFetch('schoolInfo', {
|
||||
SCHUL_NM: SCHUL_NM as string,
|
||||
});
|
||||
}
|
||||
|
||||
return result['schoolInfo']?.[1]["row"] || [];
|
||||
}
|
||||
|
||||
export async function resolveAtptCodeFromSchoolCode(SD_SCHUL_CODE: string): Promise<string> {
|
||||
const schools = await schoolInfoFetch(SD_SCHUL_CODE);
|
||||
const atptCode = schools?.[0]?.ATPT_OFCDC_SC_CODE;
|
||||
|
||||
if (!atptCode || typeof atptCode !== 'string') {
|
||||
throw new Error('학교 코드로 교육청 코드를 조회할 수 없습니다');
|
||||
}
|
||||
|
||||
return atptCode;
|
||||
}
|
||||
7
src/routes/neis/index.ts
Normal file
7
src/routes/neis/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Elysia } from 'elysia';
|
||||
|
||||
export default new Elysia({ prefix: '/neis', tags: ['NEIS'] })
|
||||
.use(import('./info'))
|
||||
.use(import('./meal'))
|
||||
.use(import('./schedule'))
|
||||
.use(import('./timetable'))
|
||||
86
src/routes/neis/info.ts
Normal file
86
src/routes/neis/info.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { Elysia, t } from 'elysia';
|
||||
import { neisFetch, resolveAtptCodeFromSchoolCode, schoolInfoFetch } from '../lib/neis';
|
||||
|
||||
type ClassSummary = {
|
||||
CLASS_NM: string;
|
||||
DDDEP_NM?: string;
|
||||
ORD_SC_NM?: string;
|
||||
};
|
||||
|
||||
type GradeGroupedClassSummary = Record<string, ClassSummary[]>;
|
||||
|
||||
function formatClassInfo(result: any): GradeGroupedClassSummary {
|
||||
const rows = result?.classInfo?.[1]?.row;
|
||||
if (!Array.isArray(rows)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return rows.reduce((acc: GradeGroupedClassSummary, row: any) => {
|
||||
if (!row?.CLASS_NM) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const grade = String(row.GRADE ?? 'unknown');
|
||||
if (!acc[grade]) {
|
||||
acc[grade] = [];
|
||||
}
|
||||
|
||||
const item: ClassSummary = {
|
||||
CLASS_NM: String(row.CLASS_NM),
|
||||
};
|
||||
|
||||
if (row.DDDEP_NM) {
|
||||
item.DDDEP_NM = String(row.DDDEP_NM);
|
||||
}
|
||||
|
||||
if (row.ORD_SC_NM) {
|
||||
item.ORD_SC_NM = String(row.ORD_SC_NM);
|
||||
}
|
||||
|
||||
acc[grade].push(item);
|
||||
return acc;
|
||||
}, {} as GradeGroupedClassSummary);
|
||||
}
|
||||
|
||||
function sortClassByName(grouped: GradeGroupedClassSummary): GradeGroupedClassSummary {
|
||||
Object.keys(grouped).forEach((grade) => {
|
||||
grouped[grade].sort((a, b) => {
|
||||
const aNum = Number(a.CLASS_NM);
|
||||
const bNum = Number(b.CLASS_NM);
|
||||
if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) {
|
||||
return aNum - bNum;
|
||||
}
|
||||
return a.CLASS_NM.localeCompare(b.CLASS_NM, 'ko', { numeric: true });
|
||||
});
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export default new Elysia({ prefix: '/info' })
|
||||
.get('/school', async ({ query }) => {
|
||||
return schoolInfoFetch(query.SD_SCHUL_CODE, query.SCHUL_NM);
|
||||
}, {
|
||||
query: t.Object({
|
||||
SCHUL_NM: t.Optional(t.String({ description: '학교 이름' })),
|
||||
SD_SCHUL_CODE: t.Optional(t.String({ description: '학교 코드' })),
|
||||
}),
|
||||
detail: { summary: '학교 정보 조회' },
|
||||
})
|
||||
.get('/class', async ({ query }) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const ATPT_OFCDC_SC_CODE = await resolveAtptCodeFromSchoolCode(query.SD_SCHUL_CODE);
|
||||
const result = await neisFetch('classInfo', {
|
||||
ATPT_OFCDC_SC_CODE,
|
||||
SD_SCHUL_CODE: query.SD_SCHUL_CODE,
|
||||
AY: currentYear.toString(),
|
||||
...(query.GRADE && { GRADE: query.GRADE }),
|
||||
});
|
||||
return sortClassByName(formatClassInfo(result));
|
||||
}, {
|
||||
query: t.Object({
|
||||
SD_SCHUL_CODE: t.String({ description: '학교 코드' }),
|
||||
GRADE: t.Optional(t.String({ description: '학년' })),
|
||||
}),
|
||||
detail: { summary: '학급 정보 조회' },
|
||||
})
|
||||
89
src/routes/neis/meal.ts
Normal file
89
src/routes/neis/meal.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { Elysia, t } from 'elysia';
|
||||
import { neisFetch, resolveAtptCodeFromSchoolCode } from '../lib/neis';
|
||||
|
||||
function verifyDateFormat(date: string) {
|
||||
if (!/^\d{8}$/.test(date)) {
|
||||
throw new Error('날짜는 YYYYMMDD 형식이어야 합니다');
|
||||
}
|
||||
}
|
||||
|
||||
function getTodayYmd(): string {
|
||||
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
}
|
||||
|
||||
function formatMealRows(rows: any[]): any[] {
|
||||
return rows.map((row) => {
|
||||
const formatted = { ...row };
|
||||
|
||||
Object.keys(formatted).forEach((key) => {
|
||||
if (typeof formatted[key] === 'string') {
|
||||
formatted[key] = formatted[key].replace(/<br\s*\/?>/gi, '\n');
|
||||
}
|
||||
});
|
||||
|
||||
delete formatted.ATPT_OFCDC_SC_CODE;
|
||||
delete formatted.ATPT_OFCDC_SC_NM;
|
||||
delete formatted.SD_SCHUL_CODE;
|
||||
delete formatted.SCHUL_NM;
|
||||
delete formatted.LOAD_DTM;
|
||||
|
||||
if (
|
||||
formatted.MLSV_FROM_YMD &&
|
||||
formatted.MLSV_TO_YMD &&
|
||||
formatted.MLSV_FROM_YMD === formatted.MLSV_TO_YMD
|
||||
) {
|
||||
formatted.MLSV_YMD = formatted.MLSV_FROM_YMD;
|
||||
delete formatted.MLSV_FROM_YMD;
|
||||
delete formatted.MLSV_TO_YMD;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
});
|
||||
}
|
||||
|
||||
export default new Elysia({ prefix: '/meal' })
|
||||
.get('/', async ({ query }) => {
|
||||
if ('MLSV_YMD' in query && ('MLSV_FROM_YMD' in query || 'MLSV_TO_YMD' in query)) {
|
||||
throw new Error('MLSV_YMD는 MLSV_FROM_YMD, MLSV_TO_YMD와 함께 사용할 수 없습니다');
|
||||
}
|
||||
const ATPT_OFCDC_SC_CODE = await resolveAtptCodeFromSchoolCode(query.SD_SCHUL_CODE);
|
||||
const { MLSV_YMD, MLSV_FROM_YMD, MLSV_TO_YMD } = query;
|
||||
if (MLSV_YMD) {
|
||||
verifyDateFormat(MLSV_YMD);
|
||||
|
||||
const result = await neisFetch('mealServiceDietInfo', {
|
||||
ATPT_OFCDC_SC_CODE,
|
||||
SD_SCHUL_CODE: query.SD_SCHUL_CODE,
|
||||
MLSV_YMD,
|
||||
});
|
||||
return formatMealRows(result['mealServiceDietInfo']?.[1]?.row || []);
|
||||
} else if (MLSV_FROM_YMD && MLSV_TO_YMD) {
|
||||
verifyDateFormat(MLSV_FROM_YMD);
|
||||
verifyDateFormat(MLSV_TO_YMD);
|
||||
const result = await neisFetch('mealServiceDietInfo', {
|
||||
ATPT_OFCDC_SC_CODE,
|
||||
SD_SCHUL_CODE: query.SD_SCHUL_CODE,
|
||||
MLSV_FROM_YMD,
|
||||
MLSV_TO_YMD,
|
||||
});
|
||||
return formatMealRows(result['mealServiceDietInfo']?.[1]?.row || []);
|
||||
} else if (!MLSV_FROM_YMD && !MLSV_TO_YMD) {
|
||||
const todayYmd = getTodayYmd();
|
||||
const result = await neisFetch('mealServiceDietInfo', {
|
||||
ATPT_OFCDC_SC_CODE,
|
||||
SD_SCHUL_CODE: query.SD_SCHUL_CODE,
|
||||
MLSV_YMD: todayYmd,
|
||||
});
|
||||
return formatMealRows(result['mealServiceDietInfo']?.[1]?.row || []);
|
||||
} else {
|
||||
throw new Error('MLSV_FROM_YMD와 MLSV_TO_YMD는 함께 제공되어야 합니다');
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
SD_SCHUL_CODE: t.String(),
|
||||
MLSV_YMD: t.Optional(t.String()),
|
||||
MLSV_FROM_YMD: t.Optional(t.String()),
|
||||
MLSV_TO_YMD: t.Optional(t.String()),
|
||||
}),
|
||||
detail: { summary: '급식 정보 조회' },
|
||||
});
|
||||
112
src/routes/neis/schedule.ts
Normal file
112
src/routes/neis/schedule.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { Elysia, t } from 'elysia';
|
||||
import { neisFetch, resolveAtptCodeFromSchoolCode } from '../lib/neis';
|
||||
|
||||
function verifyDateFormat(date: string) {
|
||||
if (!/^\d{8}$/.test(date)) {
|
||||
throw new Error('날짜는 YYYYMMDD 형식이어야 합니다');
|
||||
}
|
||||
}
|
||||
|
||||
function getTodayYmd(): string {
|
||||
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
}
|
||||
|
||||
function formatGrades(row: any): Record<string, boolean> {
|
||||
const source: Record<string, unknown> = {
|
||||
1: row.ONE_GRADE_EVENT_YN,
|
||||
2: row.TW_GRADE_EVENT_YN,
|
||||
3: row.THREE_GRADE_EVENT_YN,
|
||||
4: row.FR_GRADE_EVENT_YN,
|
||||
5: row.FIV_GRADE_EVENT_YN,
|
||||
6: row.SIX_GRADE_EVENT_YN,
|
||||
};
|
||||
|
||||
return Object.entries(source).reduce((acc: Record<string, boolean>, [grade, value]) => {
|
||||
const normalized = String(value ?? '').toUpperCase();
|
||||
if (normalized === 'Y') {
|
||||
acc[grade] = true;
|
||||
} else if (normalized === 'N') {
|
||||
acc[grade] = false;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function formatScheduleRows(rows: any[]): Record<string, Array<Record<string, unknown>>> {
|
||||
return rows.reduce((acc: Record<string, Array<Record<string, unknown>>>, row: any) => {
|
||||
const date = String(row?.AA_YMD ?? '').trim();
|
||||
const eventName = String(row?.EVENT_NM ?? '').trim();
|
||||
if (!date) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (eventName === '토요휴업일') {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[date]) {
|
||||
acc[date] = [];
|
||||
}
|
||||
|
||||
acc[date].push({
|
||||
EVENT_NM: row.EVENT_NM,
|
||||
EVENT_CNTNT: typeof row.EVENT_CNTNT === 'string'
|
||||
? row.EVENT_CNTNT.replace(/<br\s*\/?>/gi, '\n')
|
||||
: row.EVENT_CNTNT,
|
||||
SBTR_DD_SC_NM: row.SBTR_DD_SC_NM,
|
||||
grades: formatGrades(row),
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default new Elysia({ prefix: '/schedule' })
|
||||
.get('/:schoolCode', async ({ params, query }) => {
|
||||
const { schoolCode } = params;
|
||||
const { AA_YMD, AA_FROM_YMD, AA_TO_YMD } = query;
|
||||
|
||||
if (AA_YMD && (AA_FROM_YMD || AA_TO_YMD)) {
|
||||
throw new Error('AA_YMD는 AA_FROM_YMD, AA_TO_YMD와 함께 사용할 수 없습니다');
|
||||
}
|
||||
|
||||
if ((AA_FROM_YMD && !AA_TO_YMD) || (!AA_FROM_YMD && AA_TO_YMD)) {
|
||||
throw new Error('AA_FROM_YMD와 AA_TO_YMD는 함께 제공되어야 합니다');
|
||||
}
|
||||
|
||||
if (AA_YMD) {
|
||||
verifyDateFormat(AA_YMD);
|
||||
}
|
||||
|
||||
if (AA_FROM_YMD && AA_TO_YMD) {
|
||||
verifyDateFormat(AA_FROM_YMD);
|
||||
verifyDateFormat(AA_TO_YMD);
|
||||
}
|
||||
|
||||
const atptCode = await resolveAtptCodeFromSchoolCode(schoolCode);
|
||||
|
||||
const requestParams: Record<string, string> = {
|
||||
ATPT_OFCDC_SC_CODE: atptCode,
|
||||
SD_SCHUL_CODE: schoolCode,
|
||||
...(AA_YMD && { AA_YMD }),
|
||||
...(AA_FROM_YMD && AA_TO_YMD && { AA_FROM_YMD, AA_TO_YMD }),
|
||||
...(!AA_YMD && !AA_FROM_YMD && !AA_TO_YMD && { AA_YMD: getTodayYmd() }),
|
||||
};
|
||||
|
||||
const result = await neisFetch('SchoolSchedule', requestParams);
|
||||
const rows = result?.SchoolSchedule?.[1]?.row || [];
|
||||
|
||||
return formatScheduleRows(rows);
|
||||
|
||||
}, {
|
||||
params: t.Object({
|
||||
schoolCode: t.String(),
|
||||
}),
|
||||
query: t.Object({
|
||||
AA_YMD: t.Optional(t.String({ description: '조회할 날짜 (YYYYMMDD)' })),
|
||||
AA_FROM_YMD: t.Optional(t.String({ description: '조회 시작 날짜 (YYYYMMDD)' })),
|
||||
AA_TO_YMD: t.Optional(t.String({ description: '조회 종료 날짜 (YYYYMMDD)' })),
|
||||
}),
|
||||
detail: { summary: '학교 행사 일정 조회' },
|
||||
});
|
||||
108
src/routes/neis/timetable.ts
Normal file
108
src/routes/neis/timetable.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { Elysia, t } from 'elysia';
|
||||
import { neisFetch, schoolInfoFetch } from '../lib/neis';
|
||||
|
||||
type TimetableApiType = 'hisTimetable' | 'misTimetable' | 'elsTimetable' | 'spsTimetable';
|
||||
|
||||
function verifyDateFormat(date: string) {
|
||||
if (!/^\d{8}$/.test(date)) {
|
||||
throw new Error('날짜는 YYYYMMDD 형식이어야 합니다');
|
||||
}
|
||||
}
|
||||
|
||||
function getTodayYmd(): string {
|
||||
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
}
|
||||
|
||||
function resolveTimetableType(kind: string | undefined): TimetableApiType {
|
||||
switch (kind) {
|
||||
case '고등학교':
|
||||
return 'hisTimetable';
|
||||
case '중학교':
|
||||
return 'misTimetable';
|
||||
case '초등학교':
|
||||
return 'elsTimetable';
|
||||
default:
|
||||
return 'spsTimetable';
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimetableRows(rows: any[]): Record<string, Record<string, string>> {
|
||||
return rows.reduce((acc: Record<string, Record<string, string>>, row: any) => {
|
||||
const date = String(row?.ALL_TI_YMD ?? '').trim();
|
||||
const period = String(row?.PERIO ?? '').trim();
|
||||
const content = String(row?.ITRT_CNTNT ?? '').replace(/<br\s*\/?>/gi, '\n').trim();
|
||||
|
||||
if (!date || !period || !content) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[date]) {
|
||||
acc[date] = {};
|
||||
}
|
||||
|
||||
acc[date][period] = content;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default new Elysia({ prefix: '/timetable' })
|
||||
.get('/:schoolCode', async ({ params, query }) => {
|
||||
const { schoolCode } = params;
|
||||
const { grade, class: classNum, TI_FROM_YMD, TI_TO_YMD, ALL_TI_YMD } = query;
|
||||
|
||||
if ((grade && !classNum) || (!grade && classNum)) {
|
||||
throw new Error('grade와 class는 함께 제공되어야 합니다');
|
||||
}
|
||||
|
||||
if (ALL_TI_YMD && (TI_FROM_YMD || TI_TO_YMD)) {
|
||||
throw new Error('ALL_TI_YMD는 TI_FROM_YMD, TI_TO_YMD와 함께 사용할 수 없습니다');
|
||||
}
|
||||
|
||||
if ((TI_FROM_YMD && !TI_TO_YMD) || (!TI_FROM_YMD && TI_TO_YMD)) {
|
||||
throw new Error('TI_FROM_YMD와 TI_TO_YMD는 함께 제공되어야 합니다');
|
||||
}
|
||||
|
||||
if (ALL_TI_YMD) {
|
||||
verifyDateFormat(ALL_TI_YMD);
|
||||
}
|
||||
|
||||
if (TI_FROM_YMD && TI_TO_YMD) {
|
||||
verifyDateFormat(TI_FROM_YMD);
|
||||
verifyDateFormat(TI_TO_YMD);
|
||||
}
|
||||
|
||||
const schools = await schoolInfoFetch(schoolCode);
|
||||
const school = schools?.[0];
|
||||
if (!school) {
|
||||
throw new Error('학교 정보를 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
const type = resolveTimetableType(school.SCHUL_KND_SC_NM);
|
||||
|
||||
const requestParams: Record<string, string> = {
|
||||
ATPT_OFCDC_SC_CODE: String(school.ATPT_OFCDC_SC_CODE),
|
||||
SD_SCHUL_CODE: schoolCode,
|
||||
...(grade && { GRADE: grade }),
|
||||
...(classNum && { CLASS_NM: classNum }),
|
||||
...(ALL_TI_YMD && { ALL_TI_YMD }),
|
||||
...(TI_FROM_YMD && TI_TO_YMD && { TI_FROM_YMD, TI_TO_YMD }),
|
||||
...(!ALL_TI_YMD && !TI_FROM_YMD && !TI_TO_YMD && { ALL_TI_YMD: getTodayYmd() }),
|
||||
};
|
||||
|
||||
const result = await neisFetch(type, requestParams);
|
||||
const rows = result?.[type]?.[1]?.row || [];
|
||||
|
||||
return formatTimetableRows(rows);
|
||||
}, {
|
||||
params: t.Object({
|
||||
schoolCode: t.String(),
|
||||
}),
|
||||
query: t.Object({
|
||||
grade: t.String({ description: '학년' }),
|
||||
class: t.String({ description: '학급' }),
|
||||
TI_FROM_YMD: t.Optional(t.String({ description: '조회 시작 날짜 (YYYYMMDD)' })),
|
||||
TI_TO_YMD: t.Optional(t.String({ description: '조회 종료 날짜 (YYYYMMDD)' })),
|
||||
ALL_TI_YMD: t.Optional(t.String({ description: '특정 날짜의 시간표 조회 (YYYYMMDD)' })),
|
||||
}),
|
||||
detail: { summary: '시간표 조회' },
|
||||
})
|
||||
103
tsconfig.json
Normal file
103
tsconfig.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue