first commit

This commit is contained in:
암냥 2026-04-07 09:56:40 +09:00
commit 103a69afe7
No known key found for this signature in database
15 changed files with 1029 additions and 0 deletions

45
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1 @@
@imnyang:registry=https://git.mizuki.guru/api/packages/imnyang/npm/

15
README.md Normal file
View 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
View 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
View file

@ -0,0 +1,2 @@
[install.scopes]
imnyang = "https://git.mizuki.guru/api/packages/imnyang/npm/"

19
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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: '학교 행사 일정 조회' },
});

View 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
View 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. */
}
}