diff --git "a/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" "b/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" new file mode 100644 index 0000000..9c9bb7c --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" @@ -0,0 +1,10 @@ +--- +name: "[00차/찬찬]워크북 제출합니다" +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git "a/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" "b/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" index 991b3c2..1d2c061 100644 --- "a/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" +++ "b/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" @@ -2,8 +2,9 @@ name: 워크북 이슈 템플릿 about: 해당 이슈 생성 템플릿을 사용하여 워크북 이슈를 생성합니다. title: '"Chapter00 워크북 제목"' -labels: "" -assignees: seoki +labels: '' +assignees: '' + --- ### 📚 워크북 diff --git a/.gitignore b/.gitignore index f8f9e09..20f7bfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /node_modules -node_modules/ .env .env.* +dist/ +src/generated/prisma/ + -/src/generated/prisma diff --git a/package-lock.json b/package-lock.json index 366b8b3..7aa6704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,18 +11,26 @@ "dependencies": { "@prisma/adapter-mariadb": "^7.8.0", "@prisma/client": "^7.8.0", + "@tsoa/runtime": "^7.0.0-alpha.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "http-status-codes": "^2.3.0" + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1", + "swagger-ui-express": "^5.0.1", + "tsoa": "^7.0.0-alpha.0" }, "devDependencies": { "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/dotenv": "^6.1.1", "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", "@types/node": "^25.6.0", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.14", "prisma": "^7.8.0", "tsx": "^4.21.0", @@ -501,6 +509,322 @@ "node": ">=18" } }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bounce": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz", + "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/catbox-memory": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz", + "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/content": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.2.tgz", + "integrity": "sha512-OKyCOTjNR1hftwSjk9ueyAQTw8AwapvzBrPIWMGn39vhR5PmqLdYFmLc35bsSBye7gSMnlkXfc679bUdMIcRyQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.0" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz", + "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hapi": { + "version": "21.4.9", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.9.tgz", + "integrity": "sha512-YnecZOVx2AD08VvPl0ZaFS0MjEHqg+InGRmBRli731ct+VwI++dpu3BIYA1Z4SMr6HUAnpyvbQ1aq5woe3fBWg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/accept": "^6.0.3", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.2", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.2", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.2", + "@hapi/shot": "^6.0.2", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.2.1", + "@hapi/subtext": "^8.1.3", + "@hapi/teamwork": "^6.0.1", + "@hapi/topo": "^6.0.2", + "@hapi/validate": "^2.0.1" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" + } + }, + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/pez": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz", + "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/nigel": "^5.0.1" + } + }, + "node_modules/@hapi/podium": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz", + "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/shot": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz", + "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/statehood": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz", + "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/subtext": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.3.tgz", + "integrity": "sha512-WTpEZQjBP3UJ3gGunNl3w5Ao1EOJsuu2vttZ2KEcG+csSLxc0dI6VIkl2md2jDlHiQ2ARAoqdSUScy05A/NHtA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.2", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.7", + "@hapi/pez": "^6.1.1", + "@hapi/wreck": "^18.1.1" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz", + "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.2.tgz", + "integrity": "sha512-3dMnV2pfhQiyEqu8DL3VBmxkdLiRDiiUDuG79Dp+UK1gL9ZxAfDOUhB6k3D5MLqcgJJ1IARyGFhwoc1NITr/pg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -514,6 +838,23 @@ "hono": "^4" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -521,6 +862,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@prisma/adapter-mariadb": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.8.0.tgz", @@ -876,6 +1227,13 @@ } } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -883,6 +1241,400 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@tsoa/cli": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-7.0.0-alpha.0.tgz", + "integrity": "sha512-fCBWv6F20qrpwFh5X+vaZs38Bh/5cVwKPhvG/uArR+crZgEowyNl76unLCmYdvycES9Y6g/TKFLagG8qojSNuQ==", + "license": "MIT", + "dependencies": { + "@tsoa/runtime": "^7.0.0-alpha.0", + "@types/multer": "^1.4.12", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "merge-anything": "^5.1.7", + "minimatch": "^9.0.1", + "ts-deepmerge": "^7.0.2", + "typescript": "^5.7.2", + "validator": "^13.12.0", + "yaml": "^2.6.1", + "yargs": "^17.7.1" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/@tsoa/cli/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tsoa/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsoa/cli/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@tsoa/runtime": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-7.0.0-alpha.0.tgz", + "integrity": "sha512-zlWYz2bLfaN6WtFoIbLBEAyVhKG4IQKJ9QPzeRFFKeAsh5zYN4/ocnd14XyWs4ehY9TdtTnma2drW89aYNSRYw==", + "license": "MIT", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hapi": "^21.3.12", + "@types/koa": "^2.15.0", + "@types/multer": "^1.4.12", + "express": "^4.21.2", + "reflect-metadata": "^0.2.2", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/runtime/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@tsoa/runtime/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@tsoa/runtime/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", @@ -897,7 +1649,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -908,9 +1659,36 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT" + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", "dev": true, "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", "@types/node": "*" } }, @@ -938,7 +1716,6 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -950,7 +1727,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -965,35 +1741,87 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.2.tgz", + "integrity": "sha512-CB+iyjjh1uS5N6/CKwXvw0qA7USMS2WVc4Tjf660yCjhdvqzNr8gdFcIawB41zGGptOQ+d1fnpaQWIIUXYxR3w==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.19.0" } }, "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "dev": true, + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -1011,7 +1839,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1021,13 +1848,23 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1058,6 +1895,30 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1072,6 +1933,12 @@ "node": ">= 8" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -1092,6 +1959,18 @@ "node": "18 || 20 || >=22" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -1311,6 +2190,111 @@ "fsevents": "~2.3.2" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -1349,6 +2333,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -1379,7 +2382,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1457,6 +2459,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -1483,6 +2495,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1500,6 +2518,12 @@ "fast-check": "^3.23.1" } }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -1604,6 +2628,15 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1754,7 +2787,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -1785,6 +2817,20 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1819,6 +2865,15 @@ "is-property": "^1.0.2" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1886,6 +2941,27 @@ "giget": "dist/cli.mjs" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1899,6 +2975,36 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1915,7 +3021,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, "license": "ISC" }, "node_modules/grammex": { @@ -1932,6 +3037,27 @@ "devOptional": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2063,6 +3189,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2099,13 +3234,39 @@ "devOptional": true, "license": "MIT" }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -2123,6 +3284,18 @@ "devOptional": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -2213,6 +3386,21 @@ "node": ">= 0.8" } }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -2225,6 +3413,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -2257,13 +3466,74 @@ "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.8" } }, "node_modules/ms": { @@ -2294,6 +3564,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/node-addon-api": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", @@ -2393,6 +3669,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2402,6 +3687,12 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2415,12 +3706,27 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", @@ -2596,9 +3902,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2683,6 +3989,12 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/remeda": { "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", @@ -2693,6 +4005,15 @@ "url": "https://github.com/sponsors/remeda" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2739,6 +4060,12 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2827,7 +4154,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -2840,7 +4166,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -2922,7 +4247,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=14" @@ -2944,6 +4268,15 @@ "node": ">=10" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -2970,6 +4303,102 @@ "devOptional": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2983,6 +4412,30 @@ "node": ">=4" } }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3015,6 +4468,32 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-deepmerge": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz", + "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/tsoa": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-7.0.0-alpha.0.tgz", + "integrity": "sha512-o5h2DD1IKa2GF728BHYDL2uSX57a44sX8BLFScR4KbD0xlOmgsSDECneJmouDxf9MJdjHHWc+I1S3sGK82B6kQ==", + "license": "MIT", + "dependencies": { + "@tsoa/cli": "^7.0.0-alpha.0", + "@tsoa/runtime": "^7.0.0-alpha.0" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -3063,6 +4542,19 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3074,9 +4566,17 @@ "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3086,6 +4586,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/valibot": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", @@ -3101,6 +4610,15 @@ } } }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3114,7 +4632,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3126,12 +4643,201 @@ "node": ">= 8" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zeptomatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", diff --git a/package.json b/package.json index 5d951d5..ae6a745 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,26 @@ "dependencies": { "@prisma/adapter-mariadb": "^7.8.0", "@prisma/client": "^7.8.0", + "@tsoa/runtime": "^7.0.0-alpha.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "http-status-codes": "^2.3.0" + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1", + "swagger-ui-express": "^5.0.1", + "tsoa": "^7.0.0-alpha.0" }, "devDependencies": { "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/dotenv": "^6.1.1", "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", "@types/node": "^25.6.0", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.14", "prisma": "^7.8.0", "tsx": "^4.21.0", diff --git a/practice/chapter07/README.md b/practice/chapter07/README.md new file mode 100644 index 0000000..13af5b3 --- /dev/null +++ b/practice/chapter07/README.md @@ -0,0 +1,1343 @@ + +# 7주차 실습 README + +이번 7주차 실습에서는 기존 Express 프로젝트에 Tsoa를 적용하고, Express 미들웨어와 API 응답 통일, 에러 핸들링 구조를 적용했다. + +6주차까지는 Prisma ORM을 이용해 API 기능 구현에 집중했다면, 7주차에서는 API 구조를 더 일관성 있게 만들기 위해 다음 내용을 순차적으로 진행했다. + +- Tsoa 적용 +- Express 미들웨어 적용 +- morgan, cookie-parser 적용 +- 성공 응답 형식 통일 +- 실패 응답 형식 통일 +- 커스텀 Error 객체 적용 +- 전역 에러 핸들러 적용 +- Postman으로 성공/실패 응답 테스트 + +--- + +## 0. GitHub 작업 정보 + +### Issue + +```text +[Practice] 7주차 Tsoa, 미들웨어, 응답 통일 실습 +```` + +### Branch + +```text +feature/chapter-07 +``` + +### 작업 방식 + +이번 7주차 실습은 실습 코드와 README 정리가 함께 포함되므로 하나의 브랜치에서 진행했다. + +커밋은 기능별로 분리했다. + +```text +feat: 7주차 Tsoa 및 미들웨어 적용 +docs: 7주차 실습 README 정리 +docs: 7주차 미션 README 정리 +``` + +### 브랜치 이동 + +```bash +cd C:\Project\10th_Node.js_Practice_Mission + +git fetch origin +git switch feature/chapter-07 +``` + +만약 브랜치가 로컬에 없다면 다음 명령어를 사용한다. + +```bash +git switch -c feature/chapter-07 origin/feature/chapter-07 +``` + +현재 브랜치 확인: + +```bash +git branch +``` + +정상이라면 아래처럼 표시된다. + +```text +* feature/chapter-07 +``` + +--- + +## 1. 실습 전 현재 상태 확인 + +먼저 현재 프로젝트 상태를 확인했다. + +```bash +git status +``` + +올라가면 안 되는 파일이 있는지 확인한다. + +```text +.env +node_modules/ +dist/ +``` + +위 파일들은 GitHub에 올라가면 안 되므로 `.gitignore`에 포함되어 있어야 한다. + +`.gitignore` 확인: + +```gitignore +node_modules/ +.env +.env.* +dist/ +src/generated/prisma/ +``` + +--- + +## 2. Tsoa 설치 + +### 2-1. 설치 명령어 + +```bash +npm install express tsoa +``` + +설치 중 다음과 같은 경고가 나올 수 있다. + +```text +npm warn deprecated glob@10.5.0 +3 vulnerabilities +``` + +### 2-2. 로그 해석 + +이 경고는 설치 실패가 아니라, 하위 의존성 중 일부 패키지에서 deprecated 또는 보안 취약점 경고가 발생했다는 의미이다. + +설치 자체는 아래처럼 완료되면 성공이다. + +```text +added ... packages +audited ... packages +``` + +### 2-3. 설치 확인 + +```bash +npm list tsoa +npm list express +``` + +또는 Tsoa 버전 확인: + +```bash +npx tsoa --version +``` + +### 실습 인증 사진 + + +![Tsoa 설치 확인](./images/practice-01-tsoa-install.png) + +--- + +## 3. tsconfig.json 수정 + +Tsoa는 데코레이터 문법을 사용하므로 `tsconfig.json`에 아래 옵션을 추가한다. + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +이미 `compilerOptions`가 있다면 그 안에 `"experimentalDecorators": true`만 추가하면 된다. + +### 예시 + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "outDir": "./dist" + } +} +``` + +### 왜 필요한가? + +`@Route`, `@Post`, `@Get`, `@Tags` 같은 Tsoa 데코레이터를 사용하기 위해 필요하다. + +### 실습 인증 사진 + + +![tsconfig decorator 설정](./images/practice-02-tsconfig.png) + +--- + +## 4. tsoa.json 생성 + +프로젝트 루트에 `tsoa.json` 파일을 생성한다. + +```text +10th_Node.js_Practice_Mission +└── tsoa.json +``` + +내용은 다음과 같이 작성한다. + +```json +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*.controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3 + }, + "routes": { + "routesDir": "src/generated" + } +} +``` + +### 설정 설명 + +| 설정 | 의미 | +| -------------------------------- | --------------------------- | +| `entryFile` | Express 서버의 진입 파일 | +| `controllerPathGlobs` | Tsoa가 controller 파일을 찾는 경로 | +| `noImplicitAdditionalProperties` | DTO에 없는 추가 필드가 들어왔을 때 에러 처리 | +| `spec.outputDirectory` | OpenAPI 문서 생성 위치 | +| `routes.routesDir` | 자동 생성되는 routes.ts 위치 | + +### 실습 인증 사진 + + +![tsoa.json 생성](./images/practice-03-tsoa-json.png) + +--- + +## 5. 공통 응답 폴더 생성 + +성공 응답 구조를 통일하기 위해 공통 응답 파일을 만든다. + +### 폴더 생성 + +```bash +mkdir -p src/common/responses +``` + +PowerShell에서는 다음처럼 해도 된다. + +```powershell +mkdir src\common\responses +``` + +### 파일 생성 + +```text +src/common/responses/response.ts +``` + +### response.ts 작성 + +```ts +export interface ApiResponse { + resultType: "SUCCESS"; + error: null; + success: T; +} + +export const success = (data: T): ApiResponse => ({ + resultType: "SUCCESS", + error: null, + success: data, +}); +``` + +### 코드 설명 + +```ts +resultType: "SUCCESS" +``` + +요청이 성공했음을 나타낸다. + +```ts +error: null +``` + +성공 응답이므로 에러는 없다. + +```ts +success: data +``` + +실제 응답 데이터는 `success` 안에 담는다. + +--- + +## 6. 공통 Error 폴더 생성 + +실패 응답을 통일하기 위해 커스텀 Error 클래스를 만든다. + +### 폴더 생성 + +```bash +mkdir -p src/common/errors +``` + +PowerShell: + +```powershell +mkdir src\common\errors +``` + +--- + +## 7. AppError 생성 + +### 파일 생성 + +```text +src/common/errors/app.error.ts +``` + +### 코드 작성 + +```ts +export class AppError extends Error { + public readonly errorCode: string; + public readonly statusCode: number; + public readonly data?: unknown; + + constructor(params: { + errorCode: string; + message: string; + statusCode: number; + data?: unknown; + }) { + super(params.message); + + this.errorCode = params.errorCode; + this.statusCode = params.statusCode; + this.data = params.data ?? null; + } +} +``` + +### 코드 설명 + +기본 `Error` 객체는 주로 `message`만 가진다. +하지만 API 응답에서는 다음 정보가 필요하다. + +```text +errorCode +statusCode +message +data +``` + +그래서 `AppError`를 만들어 공통 에러 구조를 정의했다. + +--- + +## 8. 사용자 관련 커스텀 Error 생성 + +### 파일 생성 + +```text +src/common/errors/user.error.ts +``` + +### 코드 작성 + +```ts +import { AppError } from "./app.error.js"; + +export class DuplicateUserEmailError extends AppError { + constructor(data?: unknown) { + super({ + errorCode: "U001", + message: "이미 존재하는 이메일입니다.", + statusCode: 409, + data, + }); + } +} +``` + +### 코드 설명 + +```ts +errorCode: "U001" +``` + +이메일 중복 오류를 구분하기 위한 코드이다. + +```ts +statusCode: 409 +``` + +이미 존재하는 이메일은 기존 리소스 상태와 충돌하는 상황이므로 `409 Conflict`로 처리했다. + +--- + +## 9. User DTO 정리 + +Tsoa에서는 Controller에서 요청/응답 타입을 직접 사용하므로 DTO를 인터페이스 중심으로 정리한다. + +### 파일 위치 + +```text +src/modules/users/dtos/user.dto.ts +``` + +### 코드 예시 + +```ts +export interface UserSignUpRequest { + email: string; + password?: string; + name: string; + gender: string; + birth: Date | string; + address?: string; + detailAddress?: string; + phoneNumber: string; + preferences: number[]; +} + +export interface UserSignUpResponse { + userId: number; + preferences: string[]; +} +``` + +### 정리 + +기존에 `bodyToUser()` 같은 변환 함수가 있었다면, Tsoa 방식에서는 Controller에서 `@Body()`로 직접 타입을 받기 때문에 DTO는 요청과 응답의 타입 정의 중심으로 정리한다. + +--- + +## 10. User Service 수정 + +이메일 중복 오류가 발생할 때 일반 Error가 아니라 커스텀 Error를 던지도록 수정한다. + +### 파일 위치 + +```text +src/modules/users/services/user.service.ts +``` + +### 코드 예시 + +```ts +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { + addUser, + getUser, + getUserPreferencesByUserId, + setPreference, +} from "../repositories/user.repository.js"; +import { DuplicateUserEmailError } from "../../../common/errors/user.error.js"; + +export const userSignUp = async ( + data: UserSignUpRequest +): Promise => { + const joinUserId = await addUser({ + email: data.email, + password: data.password, + name: data.name, + gender: data.gender, + birth: new Date(data.birth), + address: data.address ?? "", + detailAddress: data.detailAddress ?? "", + phoneNumber: data.phoneNumber, + }); + + if (joinUserId === null) { + throw new DuplicateUserEmailError(data); + } + + for (const preference of data.preferences) { + await setPreference(joinUserId, preference); + } + + const user = await getUser(joinUserId); + + const preferences = (await getUserPreferencesByUserId(joinUserId)).map( + (obj) => obj.foodCategory.name + ); + + return { + userId: user.id, + preferences, + }; +}; +``` + +### 변경 전 + +```ts +throw new Error("이미 존재하는 이메일입니다."); +``` + +### 변경 후 + +```ts +throw new DuplicateUserEmailError(data); +``` + +--- + +## 11. User Controller를 Tsoa 방식으로 변경 + +기존 Express 함수형 컨트롤러를 Tsoa 클래스 기반 컨트롤러로 변경한다. + +### 파일 위치 + +```text +src/modules/users/controllers/user.controller.ts +``` + +### 기존 방식 + +```ts +export const handleUserSignUp = async (req, res, next) => { + const user = await userSignUp(req.body); + + res.status(200).json({ + result: user, + }); +}; +``` + +### Tsoa 방식 + +```ts +import { Body, Controller, Post, Route, Tags } from "tsoa"; +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { userSignUp } from "../services/user.service.js"; +import { ApiResponse, success } from "../../../common/responses/response.js"; + +@Route("users") +@Tags("Users") +export class UserController extends Controller { + @Post("signup") + public async handleUserSignUp( + @Body() body: UserSignUpRequest + ): Promise> { + console.log("회원가입을 요청했습니다!"); + console.log("body:", body); + + const user = await userSignUp(body); + + return success(user); + } +} +``` + +### 코드 설명 + +```ts +@Route("users") +``` + +이 컨트롤러의 기본 경로를 의미한다. +최종적으로 `/api/v1/users`로 연결된다. + +```ts +@Post("signup") +``` + +`POST /users/signup` 엔드포인트를 의미한다. + +```ts +@Body() body: UserSignUpRequest +``` + +요청 body를 `UserSignUpRequest` 타입으로 받는다. + +```ts +return success(user); +``` + +성공 응답을 공통 응답 형식으로 감싸서 반환한다. + +--- + +## 12. Tsoa routes 생성 + +컨트롤러를 Tsoa 방식으로 수정한 뒤 routes 파일을 생성한다. + +```bash +npx tsoa spec-and-routes +``` + +정상적으로 생성되면 아래 파일이 생긴다. + +```text +src/generated/routes.ts +``` + +### 확인 + +```bash +ls src/generated +``` + +PowerShell: + +```powershell +dir src\generated +``` + +### 실습 인증 사진 + +```md +![Tsoa routes 생성](./images/practice-04-generated-routes.png) +``` + +--- + +## 13. index.ts에 Tsoa routes 연결 + +### 파일 위치 + +```text +src/index.ts +``` + +기존에는 `index.ts`에서 직접 라우트를 등록했다. + +```ts +app.post("/api/v1/users/signup", handleUserSignUp); +``` + +이제 Tsoa가 생성한 routes를 연결한다. + +### index.ts 예시 + +```ts +import dotenv from "dotenv"; +import express, { Express, NextFunction, Request, Response } from "express"; +import cors from "cors"; +import { RegisterRoutes } from "./generated/routes.js"; +import { AppError } from "./common/errors/app.error.js"; + +dotenv.config(); + +const app: Express = express(); +const port = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +const router = express.Router(); + +RegisterRoutes(router); + +app.use("/api/v1", router); + +app.use((err: AppError, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + return next(err); + } + + res.status(err.statusCode || 500).json({ + resultType: "FAIL", + error: { + errorCode: err.errorCode || "UNKNOWN", + reason: err.message || "서버 오류가 발생했습니다.", + data: err.data || null, + }, + success: null, + }); +}); + +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); +}); +``` + +### 중요한 순서 + +```ts +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +RegisterRoutes(router); +app.use("/api/v1", router); + +app.use((err, req, res, next) => { + ... +}); +``` + +전역 에러 핸들러는 routes 등록 이후에 위치해야 한다. + +--- + +## 14. package.json scripts 수정 + +서버 실행 전 Tsoa routes를 자동 생성하도록 scripts를 수정한다. + +### package.json + +```json +{ + "scripts": { + "start": "tsoa spec-and-routes && tsx src/index.ts", + "dev": "tsoa spec-and-routes && nodemon --exec tsx src/index.ts" + } +} +``` + +### 의미 + +```text +tsoa spec-and-routes +→ src/generated/routes.ts 생성 +→ tsx src/index.ts 실행 +``` + +이렇게 하면 컨트롤러 수정 후 routes 생성을 깜빡하는 문제를 줄일 수 있다. + +--- + +## 15. 서버 실행 테스트 + +```bash +npm run dev +``` + +정상 실행 예시: + +```text +[server]: Server is running at http://localhost:3000 +``` + +### 실습 인증 사진 + +```md +![서버 실행 성공](./images/practice-05-server-run.png) +``` + +--- + +## 16. Postman으로 회원가입 성공 응답 테스트 + +### Request + +```text +POST http://localhost:3000/api/v1/users/signup +``` + +### Body + +```json +{ + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] +} +``` + +### 예상 Response + +```json +{ + "resultType": "SUCCESS", + "error": null, + "success": { + "userId": 2, + "preferences": ["한식", "중식"] + } +} +``` + +### 확인할 점 + +```text +resultType이 SUCCESS인지 확인 +error가 null인지 확인 +실제 데이터가 success 안에 들어가는지 확인 +``` + +### 실습 인증 사진 + + +![회원가입 성공 응답](./images/practice-06-signup-success.png) + + +--- + +## 17. Postman으로 중복 이메일 실패 응답 테스트 + +같은 이메일로 다시 요청을 보낸다. + +### Request + +```text +POST http://localhost:3000/api/v1/users/signup +``` + +### Body + +```json +{ + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] +} +``` + +### 예상 Response + +```json +{ + "resultType": "FAIL", + "error": { + "errorCode": "U001", + "reason": "이미 존재하는 이메일입니다.", + "data": { + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] + } + }, + "success": null +} +``` + +### 확인할 점 + +```text +resultType이 FAIL인지 확인 +errorCode가 U001인지 확인 +reason이 이미 존재하는 이메일입니다.인지 확인 +success가 null인지 확인 +HTTP Status가 409인지 확인 +``` + +### 실습 인증 사진 + + +![중복 이메일 실패 응답](./images/practice-07-signup-fail.png) + + +--- + +## 18. morgan 설치 및 적용 + +### 설치 + +```bash +npm install morgan +npm install -D @types/morgan +``` + +### index.ts 적용 + +```ts +import morgan from "morgan"; + +app.use(morgan("dev")); +``` + +`cors()` 아래, 라우터 등록 전에 작성한다. + +```ts +app.use(cors()); +app.use(morgan("dev")); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +``` + +### 확인 + +Postman으로 아무 API나 요청하면 터미널에 로그가 출력된다. + +```text +POST /api/v1/users/signup 409 12.456 ms - 350 +``` + +--- + +## 19. cookie-parser 설치 및 적용 + +### 설치 + +```bash +npm install cookie-parser +npm install -D @types/cookie-parser +``` + +### index.ts 적용 + +```ts +import cookieParser from "cookie-parser"; + +app.use(cookieParser()); +``` + +적용 위치: + +```ts +app.use(cors()); +app.use(morgan("dev")); +app.use(cookieParser()); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +``` + +--- + +## 20. 인증 미들웨어 생성 + +### 폴더 생성 + +```bash +mkdir -p src/common/middlewares +``` + +PowerShell: + +```powershell +mkdir src\common\middlewares +``` + +### 파일 생성 + +```text +src/common/middlewares/auth.middleware.ts +``` + +### 코드 작성 + +```ts +import { Request, Response, NextFunction } from "express"; + +export function authorizeUser() { + return async (req: Request, res: Response, next: NextFunction) => { + const { username } = req.cookies; + + if (username) { + console.log(`[인증 성공] ${username}님, 환영합니다.`); + next(); + } else { + console.log("[인증 실패] 로그인이 필요합니다."); + + res + .status(401) + .send( + '' + ); + } + }; +} +``` + +### 코드 설명 + +```ts +const { username } = req.cookies; +``` + +`cookie-parser`를 통해 쿠키 값을 객체로 읽는다. + +```ts +next(); +``` + +로그인 쿠키가 있으면 다음 단계로 넘어간다. + +```ts +res.status(401) +``` + +쿠키가 없으면 인증 실패로 처리한다. + +--- + +## 21. Tsoa Controller에 인증 예제 추가 + +`src/modules/users/controllers/user.controller.ts`에 아래 import를 추가한다. + +```ts +import { + Body, + Controller, + Get, + Middlewares, + Post, + Request, + Route, + Tags, +} from "tsoa"; +import { Request as ExpressRequest } from "express"; +import { authorizeUser } from "../../../common/middlewares/auth.middleware.js"; +``` + +UserController 안에 아래 메서드들을 추가한다. + +```ts +@Get("guest") +public async handleGuestPage(): Promise { + return ` +

게스트 페이지

+

이 페이지는 로그인이 필요 없습니다.

+ + `; +} + +@Get("login") +public async handleLoginPage(): Promise { + return "

로그인 페이지

로그인이 필요한 페이지에서 이동합니다.

"; +} + +@Get("mypage") +@Middlewares(authorizeUser()) +public async handleMypage( + @Request() req: ExpressRequest +): Promise { + return ` +

마이페이지

+

환영합니다, ${req.cookies.username}님!

+

이 페이지는 로그인한 사람만 볼 수 있습니다.

+ `; +} + +@Get("set-login") +public async handleSetLogin( + @Request() req: ExpressRequest +): Promise { + req.res!.cookie("username", "UMC10th", { maxAge: 3600000 }); + + return '로그인 쿠키 생성 완료! 마이페이지로 이동'; +} + +@Get("set-logout") +public async handleSetLogout( + @Request() req: ExpressRequest +): Promise { + req.res!.clearCookie("username"); + + return '로그아웃 완료! 게스트 페이지로 이동'; +} +``` + +--- + +## 22. routes 재생성 및 서버 재실행 + +Controller에 새로운 endpoint를 추가했으므로 routes를 다시 생성해야 한다. + +```bash +npx tsoa spec-and-routes +``` + +또는 서버 실행 시 자동 생성되도록 했다면: + +```bash +npm run dev +``` + +--- + +## 23. 브라우저로 인증 미들웨어 테스트 + +브라우저에서 아래 순서대로 접속한다. + +### 1단계. 게스트 페이지 접속 + +```text +http://localhost:3000/api/v1/users/guest +``` + +### 2단계. 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +쿠키가 없으면 로그인 필요 alert가 뜨고 `/api/v1/users/login`으로 이동한다. + +### 3단계. 로그인 쿠키 생성 + +```text +http://localhost:3000/api/v1/users/set-login +``` + +### 4단계. 다시 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +이번에는 정상적으로 마이페이지가 열린다. + +### 5단계. 로그아웃 + +```text +http://localhost:3000/api/v1/users/set-logout +``` + +### 6단계. 다시 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +다시 로그인 필요 alert가 뜨면 성공이다. + +### 실습 인증 사진 + + +![게스트 페이지](./images/practice-09-guest-page.png) +![로그인 필요 화면](./images/practice-10-login-required.png) +![쿠키 생성](./images/practice-11-set-login.png) +![마이페이지 성공](./images/practice-12-mypage-success.png) +![로그아웃](./images/practice-13-logout.png) + + +--- + +## 24. 실습 완료 후 최종 확인 + +### 서버 실행 + +```bash +npm run dev +``` + +### 확인해야 할 API + +```text +POST /api/v1/users/signup +GET /api/v1/users/guest +GET /api/v1/users/login +GET /api/v1/users/mypage +GET /api/v1/users/set-login +GET /api/v1/users/set-logout +``` + +### 확인해야 할 내용 + +```text +[ ] Tsoa routes가 생성되는가? +[ ] 서버가 정상 실행되는가? +[ ] 회원가입 성공 응답이 SUCCESS 구조로 반환되는가? +[ ] 중복 이메일 실패 응답이 FAIL 구조로 반환되는가? +[ ] morgan 로그가 터미널에 찍히는가? +[ ] cookie-parser로 쿠키를 읽을 수 있는가? +[ ] 인증 미들웨어가 mypage 접근을 제어하는가? +``` + +--- + +## 25. 실습 중 발생한 이슈 정리 + +### 이슈 1. tsoa 설치 중 deprecated 및 vulnerabilities 경고 발생 + +#### 문제 + +```text +npm warn deprecated glob@10.5.0 +3 vulnerabilities +``` + +#### 원인 + +`tsoa` 설치 과정에서 함께 설치되는 하위 의존성 중 일부 패키지에서 deprecated 경고와 보안 취약점 경고가 발생했다. + +#### 해결 + +설치 자체는 정상 완료되었는지 먼저 확인했다. + +```bash +npm list tsoa +npm list express +``` + +이후 취약점 상세 내용은 아래 명령어로 확인했다. + +```bash +npm audit +``` + +`npm audit fix --force`는 major version 변경으로 프로젝트가 깨질 수 있으므로 바로 실행하지 않았다. + +--- + +### 이슈 2. generated/routes.ts를 찾을 수 없음 + +#### 문제 + +```text +Cannot find module './generated/routes' +``` + +#### 원인 + +Tsoa routes 파일을 아직 생성하지 않았다. + +#### 해결 + +```bash +npx tsoa spec-and-routes +``` + +또는 `package.json` scripts에 아래처럼 추가했다. + +```json +"dev": "tsoa spec-and-routes && nodemon --exec tsx src/index.ts" +``` + +--- + +### 이슈 3. 데코레이터 오류 발생 + +#### 문제 + +`@Route`, `@Post` 같은 데코레이터 사용 시 TypeScript 오류가 발생했다. + +#### 원인 + +`tsconfig.json`에 데코레이터 사용 설정이 없었다. + +#### 해결 + +```json +"experimentalDecorators": true +``` + +--- + +### 이슈 4. 실패 응답이 UNKNOWN으로만 내려옴 + +#### 문제 + +```json +{ + "resultType": "FAIL", + "error": { + "errorCode": "UNKNOWN", + "reason": "이미 존재하는 이메일입니다.", + "data": null + }, + "success": null +} +``` + +#### 원인 + +일반 `Error` 객체에는 `errorCode`, `statusCode`가 없기 때문이다. + +#### 해결 + +`AppError`를 상속한 커스텀 Error를 만들고 Service에서 해당 Error를 던지도록 수정했다. + +```ts +throw new DuplicateUserEmailError(data); +``` + +--- + +## 26. 실습 인증 체크리스트 + +아래 사진을 `practice/chapter07/images/`에 저장한다. + +```text +[ ] practice-01-tsoa-install.png +[ ] practice-02-tsconfig.png +[ ] practice-03-tsoa-json.png +[ ] practice-04-generated-routes.png +[ ] practice-05-server-run.png +[ ] practice-06-signup-success.png +[ ] practice-07-signup-fail.png +[ ] practice-08-morgan-log.png +[ ] practice-09-guest-page.png +[ ] practice-10-login-required.png +[ ] practice-11-set-login.png +[ ] practice-12-mypage-success.png +[ ] practice-13-logout.png +``` + +--- + +## 27. Git commit 정리 + +### 실습 코드 커밋 + +```bash +git add src package.json package-lock.json tsconfig.json tsoa.json +git commit -m "feat: 7주차 Tsoa 및 미들웨어 적용" +``` + +### 실습 README 커밋 + +```bash +git add practice/chapter07/README.md +git add practice/chapter07/images/ +git commit -m "docs: 7주차 실습 README 정리" +``` + +### Push + +```bash +git push -u origin feature/chapter-07 +``` + +--- + +## 28. 느낀 점 + +이번 실습을 통해 API는 단순히 기능이 동작하는 것뿐만 아니라, 응답 구조와 에러 처리 방식도 일관되게 관리해야 한다는 것을 느꼈다. + +Tsoa를 적용하면서 컨트롤러에서 API 경로와 요청 타입, 응답 타입을 더 명확하게 표현할 수 있었다. 또한 `success()` wrapper와 `AppError`를 적용하면서 성공 응답과 실패 응답의 구조를 통일할 수 있었다. + +`morgan`을 통해 요청 로그를 확인하고, `cookie-parser`와 인증 미들웨어를 통해 특정 라우트를 보호하는 흐름도 이해할 수 있었다. + +이번 실습을 통해 Express의 미들웨어 구조가 단순 보조 기능이 아니라 서버 요청 처리 흐름의 핵심이라는 점을 알게 되었다. + +```` diff --git a/practice/chapter07/images/practice-01-tsoa-install.png b/practice/chapter07/images/practice-01-tsoa-install.png new file mode 100644 index 0000000..ad63b58 Binary files /dev/null and b/practice/chapter07/images/practice-01-tsoa-install.png differ diff --git a/practice/chapter07/images/practice-02-tsconfig.png b/practice/chapter07/images/practice-02-tsconfig.png new file mode 100644 index 0000000..aefb543 Binary files /dev/null and b/practice/chapter07/images/practice-02-tsconfig.png differ diff --git a/practice/chapter07/images/practice-03-tsoa-json.png b/practice/chapter07/images/practice-03-tsoa-json.png new file mode 100644 index 0000000..3e8468f Binary files /dev/null and b/practice/chapter07/images/practice-03-tsoa-json.png differ diff --git a/practice/chapter07/images/practice-06-signup-success.png b/practice/chapter07/images/practice-06-signup-success.png new file mode 100644 index 0000000..ccd4eeb Binary files /dev/null and b/practice/chapter07/images/practice-06-signup-success.png differ diff --git a/practice/chapter07/images/practice-07-signup-fail.png b/practice/chapter07/images/practice-07-signup-fail.png new file mode 100644 index 0000000..71efb96 Binary files /dev/null and b/practice/chapter07/images/practice-07-signup-fail.png differ diff --git a/practice/chapter07/images/practice-09-guest-page.png b/practice/chapter07/images/practice-09-guest-page.png new file mode 100644 index 0000000..eaec4db Binary files /dev/null and b/practice/chapter07/images/practice-09-guest-page.png differ diff --git a/practice/chapter07/images/practice-10-login-required.png b/practice/chapter07/images/practice-10-login-required.png new file mode 100644 index 0000000..fa0897d Binary files /dev/null and b/practice/chapter07/images/practice-10-login-required.png differ diff --git a/practice/chapter07/images/practice-11-set-login.png b/practice/chapter07/images/practice-11-set-login.png new file mode 100644 index 0000000..2af5cf8 Binary files /dev/null and b/practice/chapter07/images/practice-11-set-login.png differ diff --git a/practice/chapter07/images/practice-12-mypage-success.png b/practice/chapter07/images/practice-12-mypage-success.png new file mode 100644 index 0000000..283481b Binary files /dev/null and b/practice/chapter07/images/practice-12-mypage-success.png differ diff --git a/practice/chapter07/images/practice-13-logout.png b/practice/chapter07/images/practice-13-logout.png new file mode 100644 index 0000000..ff38876 Binary files /dev/null and b/practice/chapter07/images/practice-13-logout.png differ diff --git a/practice/chapter08/README.md b/practice/chapter08/README.md new file mode 100644 index 0000000..eac27ff --- /dev/null +++ b/practice/chapter08/README.md @@ -0,0 +1,286 @@ +# 8주차 실습 README + +## 1. 실습 목표 + +8주차 실습의 목표는 프론트엔드에서 백엔드 API를 직접 호출해보며 CORS 상황을 이해하고, Swagger UI를 통해 API 문서를 확인하는 것이다. + +이번 실습에서는 다음 내용을 확인하였다. + +- HTML 파일에서 백엔드 회원가입 API 호출 +- 브라우저 개발자 도구 Console에서 성공/실패 응답 확인 +- `swagger-ui-express`를 이용한 Swagger UI 연결 +- TSOA가 생성한 `swagger.json` 기반 API 문서 확인 + +--- + +## 2. CORS 실습용 `test.html` 작성 + +프론트엔드에서 백엔드 API를 호출하는 상황을 확인하기 위해 `test.html` 파일을 작성하였다. + +백엔드 서버는 `http://localhost:3000`에서 실행되고, `test.html`은 VSCode Live Server를 통해 다른 포트에서 실행된다. +이를 통해 서로 다른 Origin에서 API를 호출하는 상황을 만들 수 있다. + +```html + + + + + 회원가입 API 테스트 + + +

회원가입 테스트

+ + + + + + + +``` + +--- + +## 3. Live Server로 실행 + +작성한 `test.html` 파일을 VSCode의 Live Server로 실행하였다. + +실행 후 브라우저에서 아래와 같은 주소로 접속되었다. + +```text +http://127.0.0.1:5500/practice/chapter08/test.html +``` + +백엔드 서버는 `http://localhost:3000`에서 실행 중이므로, 프론트엔드와 백엔드가 서로 다른 Origin에서 통신하는 상황이 된다. + +### 실행 화면 + +![Live Server 테스트 페이지](./images/03-live-server-test-page.png) + +--- + +## 4. 회원가입 API 성공 응답 확인 + +`가입 요청 성공` 버튼을 클릭하면 매번 새로운 이메일을 생성하여 회원가입 API를 호출한다. + +```js +email: `test_${Date.now()}@test.com` +``` + +이 방식은 이메일 중복을 피하기 위해 사용하였다. + +브라우저 개발자 도구의 Console에서 요청 데이터와 성공 응답을 확인할 수 있었다. + +### 성공 응답 확인 화면 + +![CORS 성공 응답](./images/04-cors-success-response.png) + +--- + +## 5. 회원가입 API 실패 응답 확인 + +`가입 요청 실패` 버튼을 클릭하면 이미 존재한다고 가정한 이메일로 회원가입 요청을 보낸다. + +```js +email: "test@example.com" +``` + +이 요청은 중복 이메일 상황을 확인하기 위한 것이다. +브라우저 개발자 도구의 Console에서 실패 응답을 확인할 수 있었다. + +이를 통해 7주차에서 구현한 표준 에러 응답이 프론트엔드에서 어떻게 전달되는지 확인할 수 있었다. + +### 실패 응답 확인 화면 + +![CORS 실패 응답](./images/05-cors-fail-response.png) + +--- + +## 6. Swagger UI 패키지 설치 + +Swagger UI를 Express 서버에서 확인하기 위해 다음 패키지를 설치하였다. + +```bash +npm install swagger-ui-express +npm install --save-dev @types/swagger-ui-express +``` + +설치 후 `package.json`에 관련 의존성이 추가되었다. + +--- + +## 7. TSOA Swagger 명세 생성 + +TSOA를 이용해 Swagger 명세 파일을 생성하였다. + +```bash +npx tsoa spec +``` + +명령어 실행 후 `dist/swagger.json` 파일이 생성된다. + +```text +dist/swagger.json +``` + +이 파일은 OpenAPI 형식으로 작성된 API 명세 파일이며, Swagger UI는 이 파일을 읽어서 API 문서를 화면에 보여준다. + +### Swagger 명세 생성 화면 + +![TSOA 명세 생성](./images/07-tsoa-spec-generated.png) + +--- + +## 8. Swagger UI 연결 + +`src/index.ts`에서 TSOA가 생성한 `dist/swagger.json` 파일을 읽어 `/docs` 경로에 Swagger UI를 연결하였다. + +```ts +import swaggerUi from "swagger-ui-express"; +import path from "path"; +import fs from "fs"; + +const swaggerFile = JSON.parse( + fs.readFileSync(path.resolve("dist/swagger.json"), "utf8") +); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); +``` + +위 코드에서 `dist/swagger.json`을 읽어오고, `/docs` 경로에서 Swagger UI가 보이도록 설정하였다. + +--- + +## 9. Swagger UI 확인 + +서버를 실행한 뒤 브라우저에서 아래 주소로 접속하였다. + +```text +http://localhost:3000/docs +``` + +Swagger UI에서 현재 프로젝트의 API 목록을 확인할 수 있었다. + +### Swagger UI 메인 화면 + +![Swagger UI 메인 화면](./images/08-swagger-ui-main.png) + +--- + +## 10. API 상세 문서 확인 + +Swagger UI에서 특정 API를 펼쳐 요청 Body, 응답 형식, 상태 코드 등을 확인하였다. + +특히 회원가입 API의 경우 요청 Body에 필요한 필드와 성공/실패 응답을 확인할 수 있었다. + +### API 상세 화면 + +![Swagger API 상세 화면](./images/09-swagger-api-detail.png) + +--- + +## 11. 실습 정리 + +이번 실습을 통해 프론트엔드가 백엔드 API를 직접 호출할 때 어떤 방식으로 요청과 응답이 오가는지 확인할 수 있었다. + +특히 `test.html`을 Live Server로 실행하여 백엔드 서버와 다른 Origin에서 API를 호출함으로써 CORS 상황을 직접 확인할 수 있었다. 또한 Swagger UI를 연결하면서 API 문서화가 프론트엔드와의 협업에서 중요하다는 점을 이해할 수 있었다. + +--- + +## 12. 트러블슈팅 + +### 이슈 1. Swagger UI 실행 시 `dist/swagger.json` 파일을 찾을 수 없는 문제 + +#### 문제 + +Swagger UI 설정 코드에서 `dist/swagger.json` 파일을 읽도록 작성했지만, 아직 해당 파일이 생성되지 않은 경우 서버 실행 시 오류가 발생할 수 있다. + +#### 해결 + +서버 실행 전에 아래 명령어를 먼저 실행하여 Swagger 명세 파일을 생성하였다. + +```bash +npx tsoa spec +``` + +--- + +### 이슈 2. CORS 실습에서 API 요청이 실패하는 문제 + +#### 문제 + +`test.html`은 Live Server에서 실행되고 백엔드 서버는 `localhost:3000`에서 실행되기 때문에 서로 다른 Origin에서 요청이 발생한다. +이때 서버에서 CORS 설정이 되어 있지 않으면 브라우저가 요청을 차단할 수 있다. + +#### 해결 + +Express 서버에서 `cors` 미들웨어가 등록되어 있는지 확인하였다. + +```ts +app.use(cors()); +``` + +이를 통해 다른 Origin에서 들어오는 요청을 허용할 수 있었다. + +--- + +## 13. 느낀 점 + +이번 실습을 통해 API는 단순히 백엔드에서 구현하는 것만으로 끝나는 것이 아니라, 프론트엔드가 실제로 호출하고 이해할 수 있어야 한다는 점을 알게 되었다. + +CORS 실습을 통해 프론트엔드와 백엔드가 서로 다른 Origin에서 통신할 때 발생할 수 있는 문제를 확인할 수 있었고, Swagger UI를 통해 API 문서를 제공하면 협업 과정에서 요청 방식과 응답 구조를 훨씬 명확하게 공유할 수 있다는 점을 이해할 수 있었다. \ No newline at end of file diff --git a/practice/chapter08/images/03-live-server-test-page.png b/practice/chapter08/images/03-live-server-test-page.png new file mode 100644 index 0000000..c101676 Binary files /dev/null and b/practice/chapter08/images/03-live-server-test-page.png differ diff --git a/practice/chapter08/images/04-cors-success-response.png b/practice/chapter08/images/04-cors-success-response.png new file mode 100644 index 0000000..23c1fa2 Binary files /dev/null and b/practice/chapter08/images/04-cors-success-response.png differ diff --git a/practice/chapter08/images/05-cors-fail-response.png b/practice/chapter08/images/05-cors-fail-response.png new file mode 100644 index 0000000..7ec02eb Binary files /dev/null and b/practice/chapter08/images/05-cors-fail-response.png differ diff --git a/practice/chapter08/images/07-tsoa-spec-generated.png b/practice/chapter08/images/07-tsoa-spec-generated.png new file mode 100644 index 0000000..68a6311 Binary files /dev/null and b/practice/chapter08/images/07-tsoa-spec-generated.png differ diff --git a/practice/chapter08/images/08-swagger-ui-main.png b/practice/chapter08/images/08-swagger-ui-main.png new file mode 100644 index 0000000..eb88a5d Binary files /dev/null and b/practice/chapter08/images/08-swagger-ui-main.png differ diff --git a/practice/chapter08/images/09-swagger-api-detail.png b/practice/chapter08/images/09-swagger-api-detail.png new file mode 100644 index 0000000..b5dbac5 Binary files /dev/null and b/practice/chapter08/images/09-swagger-api-detail.png differ diff --git a/practice/chapter08/test.html b/practice/chapter08/test.html new file mode 100644 index 0000000..39d9b72 --- /dev/null +++ b/practice/chapter08/test.html @@ -0,0 +1,71 @@ + + + + + 회원가입 API 테스트 + + +

회원가입 테스트

+ + + + + + + \ No newline at end of file diff --git a/src/common/errors/app.error.ts b/src/common/errors/app.error.ts new file mode 100644 index 0000000..2b2d955 --- /dev/null +++ b/src/common/errors/app.error.ts @@ -0,0 +1,18 @@ +export class AppError extends Error { + public readonly errorCode: string; + public readonly statusCode: number; + public readonly data?: unknown; + + constructor(params: { + errorCode: string; + message: string; + statusCode: number; + data?: unknown; + }) { + super(params.message); + + this.errorCode = params.errorCode; + this.statusCode = params.statusCode; + this.data = params.data ?? null; + } +} \ No newline at end of file diff --git a/src/common/errors/user.error.ts b/src/common/errors/user.error.ts new file mode 100644 index 0000000..0202ff8 --- /dev/null +++ b/src/common/errors/user.error.ts @@ -0,0 +1,12 @@ +import { AppError } from "./app.error.js"; + +export class DuplicateUserEmailError extends AppError { + constructor(data?: unknown) { + super({ + errorCode: "U001", + message: "이미 존재하는 이메일입니다.", + statusCode: 409, + data, + }); + } +} \ No newline at end of file diff --git a/src/common/middlewares/auth.middleware.ts b/src/common/middlewares/auth.middleware.ts new file mode 100644 index 0000000..deab7dd --- /dev/null +++ b/src/common/middlewares/auth.middleware.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from "express"; + +export function authorizeUser() { + return async (req: Request, res: Response, next: NextFunction) => { + const { username } = req.cookies; + + if (username) { + console.log(`[인증 성공] ${username}님, 환영합니다.`); + next(); + return; + } + + console.log("[인증 실패] 로그인이 필요합니다."); + + res + .status(401) + .send( + '' + ); + }; +} \ No newline at end of file diff --git a/src/common/responses/response.ts b/src/common/responses/response.ts new file mode 100644 index 0000000..9469334 --- /dev/null +++ b/src/common/responses/response.ts @@ -0,0 +1,11 @@ +export interface ApiResponse { + resultType: "SUCCESS"; + error: null; + success: T; +} + +export const success = (data: T): ApiResponse => ({ + resultType: "SUCCESS", + error: null, + success: data, +}); \ No newline at end of file diff --git a/src/generated/routes.ts b/src/generated/routes.ts new file mode 100644 index 0000000..9fff9b6 --- /dev/null +++ b/src/generated/routes.ts @@ -0,0 +1,252 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import type { TsoaRoute } from '@tsoa/runtime'; +import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { UserController } from './../modules/users/controllers/user.controller'; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; + + + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "UserSignUpResponse": { + "dataType": "refObject", + "properties": { + "userId": {"dataType":"double","required":true}, + "preferences": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_UserSignUpResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"enum","enums":["SUCCESS"],"required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"UserSignUpResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserSignUpRequest": { + "dataType": "refObject", + "properties": { + "email": {"dataType":"string","required":true}, + "password": {"dataType":"string"}, + "name": {"dataType":"string","required":true}, + "gender": {"dataType":"string","required":true}, + "birth": {"dataType":"string","required":true}, + "address": {"dataType":"string"}, + "detailAddress": {"dataType":"string"}, + "phoneNumber": {"dataType":"string","required":true}, + "preferences": {"dataType":"array","array":{"dataType":"double"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + + +export function RegisterRoutes(app: Router) { + + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + + + const argsUserController_handleUserSignUp: Record = { + body: {"in":"body","name":"body","required":true,"ref":"UserSignUpRequest"}, + }; + app.post('/users/signup', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleUserSignUp)), + + async function UserController_handleUserSignUp(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleUserSignUp, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleUserSignUp', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleGuestPage: Record = { + }; + app.get('/users/guest', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleGuestPage)), + + async function UserController_handleGuestPage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleGuestPage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleGuestPage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleLoginPage: Record = { + }; + app.get('/users/login', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleLoginPage)), + + async function UserController_handleLoginPage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleLoginPage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleLoginPage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleMypage: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/mypage', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleMypage)), + + async function UserController_handleMypage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleMypage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleMypage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleSetLogin: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/set-login', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleSetLogin)), + + async function UserController_handleSetLogin(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleSetLogin, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleSetLogin', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleSetLogout: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/set-logout', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleSetLogout)), + + async function UserController_handleSetLogout(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleSetLogout, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleSetLogout', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/index.ts b/src/index.ts index 5be4e5d..e1f1c94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,14 @@ import dotenv from "dotenv"; -import express, { Express, Request, Response, NextFunction } from "express"; +import express, { Express, NextFunction, Request, Response } from "express"; import cors from "cors"; +import morgan from "morgan"; +import cookieParser from "cookie-parser"; +import swaggerUi from "swagger-ui-express"; +import path from "path"; +import fs from "fs"; -import { handleUserSignUp } from "./modules/users/controllers/user.controller.js"; -import { handleCreateStore } from "./modules/stores/controllers/store.controller.js"; -import { - handleCreateReview, - handleListMyReviews, -} from "./modules/reviews/controllers/review.controller.js"; -import { - handleCreateMission, - handleChallengeMission, - handleListStoreMissions, - handleListInProgressMissions, - handleCompleteMission, -} from "./modules/missions/controllers/mission.controller.js"; +import { RegisterRoutes } from "./generated/routes.js"; +import { AppError } from "./common/errors/app.error.js"; dotenv.config(); @@ -25,6 +19,8 @@ const port = process.env.PORT || 3000; * 공통 미들웨어 설정 */ app.use(cors()); +app.use(morgan("dev")); +app.use(cookieParser()); app.use(express.static("public")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -36,34 +32,31 @@ app.get("/", (req: Request, res: Response) => { res.send("Hello World! This is TypeScript Server!"); }); -app.post("/api/v1/users/signup", handleUserSignUp); - -app.post("/api/v1/regions/:regionId/stores", handleCreateStore); - -app.post("/api/v1/stores/:storeId/reviews", handleCreateReview); +/** + * Tsoa가 자동 생성한 routes 등록 + */ +const router = express.Router(); -app.get("/api/v1/users/:userId/reviews", handleListMyReviews); +RegisterRoutes(router); -app.post("/api/v1/stores/:storeId/missions", handleCreateMission); -app.get("/api/v1/stores/:storeId/missions", handleListStoreMissions); -app.get( - "/api/v1/users/:userId/missions/in-progress", - handleListInProgressMissions -); -app.patch( - "/api/v1/users/:userId/missions/:missionId/complete", - handleCompleteMission -); -app.post("/api/v1/missions/:missionId/challenge", handleChallengeMission); +app.use("/api/v1", router); /** - * 공통 에러 처리 미들웨어 + * 전역 에러 핸들러 */ -app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - console.error("에러 발생:", err.message); +app.use((err: AppError, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + return next(err); + } - res.status(400).json({ - error: err.message, + res.status(err.statusCode || 500).json({ + resultType: "FAIL", + error: { + errorCode: err.errorCode || "UNKNOWN", + reason: err.message || "서버 오류가 발생했습니다.", + data: err.data || null, + }, + success: null, }); }); @@ -72,4 +65,10 @@ app.use((err: Error, req: Request, res: Response, next: NextFunction) => { */ app.listen(port, () => { console.log(`[server]: Server is running at http://localhost:${port}`); -}); \ No newline at end of file +}); + +const swaggerFile = JSON.parse( + fs.readFileSync(path.resolve("dist/swagger.json"), "utf8") +); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); \ No newline at end of file diff --git a/src/modules/users/controllers/user.controller.ts b/src/modules/users/controllers/user.controller.ts index 96872be..2010fc8 100644 --- a/src/modules/users/controllers/user.controller.ts +++ b/src/modules/users/controllers/user.controller.ts @@ -1,23 +1,94 @@ -import { Request, Response, NextFunction } from "express"; -import { StatusCodes } from "http-status-codes"; -import { bodyToUser } from "../dtos/user.dto.js"; +import { + Body, + Controller, + Get, + Middlewares, + Post, + Request, + Route, + Tags, +} from "tsoa"; +import { Request as ExpressRequest } from "express"; + +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; import { userSignUp } from "../services/user.service.js"; +import { ApiResponse, success } from "../../../common/responses/response.js"; +import { authorizeUser } from "../../../common/middlewares/auth.middleware.js"; -export const handleUserSignUp = async ( - req: Request, - res: Response, - next: NextFunction -) => { - try { +@Route("users") +@Tags("Users") +export class UserController extends Controller { + @Post("signup") + public async handleUserSignUp( + @Body() body: UserSignUpRequest + ): Promise> { console.log("회원가입을 요청했습니다!"); - console.log("body:", req.body); + console.log("body:", body); + + const user = await userSignUp(body); + + return success(user); + } - const user = await userSignUp(bodyToUser(req.body)); + @Get("guest") + public async handleGuestPage(): Promise { + return ` +

게스트 페이지

+

이 페이지는 로그인이 필요 없습니다.

+ + `; + } + + @Get("login") + public async handleLoginPage(): Promise { + return ` +

로그인 페이지

+

로그인이 필요한 페이지에서 이동합니다.

+ 로그인 쿠키 생성하기 + `; + } - res.status(StatusCodes.OK).json({ - result: user, + @Get("mypage") + @Middlewares(authorizeUser()) + public async handleMypage( + @Request() req: ExpressRequest + ): Promise { + return ` +

마이페이지

+

환영합니다, ${req.cookies.username}님!

+

이 페이지는 로그인한 사람만 볼 수 있습니다.

+ 로그아웃 + `; + } + + @Get("set-login") + public async handleSetLogin( + @Request() req: ExpressRequest + ): Promise { + req.res!.cookie("username", "UMC10th", { + maxAge: 3600000, + httpOnly: true, }); - } catch (error) { - next(error); + + return ` +

로그인 쿠키 생성 완료

+

username=UMC10th 쿠키가 생성되었습니다.

+ 마이페이지로 이동 + `; + } + + @Get("set-logout") + public async handleSetLogout( + @Request() req: ExpressRequest + ): Promise { + req.res!.clearCookie("username"); + + return ` +

로그아웃 완료

+

쿠키가 삭제되었습니다.

+ 게스트 페이지로 이동 + `; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/modules/users/dtos/user.dto.ts b/src/modules/users/dtos/user.dto.ts index c89c108..6a75302 100644 --- a/src/modules/users/dtos/user.dto.ts +++ b/src/modules/users/dtos/user.dto.ts @@ -1,51 +1,20 @@ export interface UserSignUpRequest { email: string; + password?: string; name: string; gender: string; - birth: string; - address?: string; - detailAddress?: string; - phoneNumber: string; - preferences: number[]; -} - -export const bodyToUser = (body: UserSignUpRequest) => { - return { - email: body.email, - password: body.password, - name: body.name, - gender: body.gender, - birth: new Date(body.birth), - address: body.address || "", - detailAddress: body.detailAddress || "", - phoneNumber: body.phoneNumber, - preferences: body.preferences, - }; -}; -export const responseFromUser = ({ - user, - preferences, -}: { - user: any; - preferences: any[]; -}) => { - return { - email: user.email, - name: user.name, - preferCategory: preferences.map((preference) => preference.foodCategory.name), - }; -}; - -export interface UserSignUpRequest { - email: string; - password: string; - name: string; - gender: string; + // JSON 요청에서는 날짜가 문자열로 들어오므로 string으로 받는 것이 안전합니다. + // Service에서 new Date(data.birth)로 Date 객체로 변환합니다. birth: string; + address?: string; detailAddress?: string; phoneNumber: string; preferences: number[]; } +export interface UserSignUpResponse { + userId: number; + preferences: string[]; +} \ No newline at end of file diff --git a/src/modules/users/services/user.service.ts b/src/modules/users/services/user.service.ts index bf2fdae..0abd917 100644 --- a/src/modules/users/services/user.service.ts +++ b/src/modules/users/services/user.service.ts @@ -1,46 +1,51 @@ +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; import { addUser, getUser, getUserPreferencesByUserId, setPreference, } from "../repositories/user.repository.js"; -import { responseFromUser } from "../dtos/user.dto.js"; -import bcrypt from "bcrypt"; - -export const userSignUp = async (data: { - email: string; - password: string; - name: string; - gender: string; - birth: Date; - address: string; - detailAddress: string; - phoneNumber: string; - preferences: number[]; -}) => { - const hashedPassword = await bcrypt.hash(data.password, 10); +import { DuplicateUserEmailError } from "../../../common/errors/user.error.js"; +export const userSignUp = async ( + data: UserSignUpRequest +): Promise => { const joinUserId = await addUser({ email: data.email, - password: hashedPassword, + password: data.password, name: data.name, gender: data.gender, - birth: data.birth, - address: data.address, - detailAddress: data.detailAddress, + + // 요청 body에서는 birth가 문자열로 들어오기 때문에 Date 객체로 변환합니다. + birth: new Date(data.birth), + + // 선택값은 undefined로 들어올 수 있으므로 기본값을 넣어줍니다. + address: data.address ?? "", + detailAddress: data.detailAddress ?? "", phoneNumber: data.phoneNumber, }); + // addUser에서 이메일 중복이면 null을 반환하도록 되어 있으므로, + // 이 경우 커스텀 에러를 던집니다. if (joinUserId === null) { - throw new Error("이미 존재하는 이메일입니다."); + throw new DuplicateUserEmailError(data); } + // 사용자가 선택한 음식 선호 카테고리를 저장합니다. for (const preference of data.preferences) { await setPreference(joinUserId, preference); } + // 생성된 사용자 정보를 다시 조회합니다. const user = await getUser(joinUserId); - const preferences = await getUserPreferencesByUserId(joinUserId); - return responseFromUser({ user, preferences }); + // 사용자의 선호 카테고리 이름 목록을 조회합니다. + const preferences = (await getUserPreferencesByUserId(joinUserId)).map( + (obj) => obj.foodCategory.name + ); + + return { + userId: user.id, + preferences, + }; }; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3d72177..fc94250 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,25 +1,115 @@ { "compilerOptions": { - /* 프로젝트 구조 설정 */ - "rootDir": "./src", // 소스 코드(.ts)가 모여있는 폴더예요. - "outDir": "./dist", // 컴파일된 결과물(.js)이 저장될 폴더예요. - - /* 언어 및 모듈 설정 (4주차에서 배운 ESM 방식을 유지해요!) */ - "target": "ESNext", - "module": "NodeNext", // 최신 Node.js의 모듈 시스템(ESM)을 사용합니다. - "moduleResolution": "NodeNext", - "lib": ["ESNext"], // 최신 자바스크립트 표준 기능을 사용할 수 있게 해줘요. - - /* 엄격한 검사 옵션 (에러를 미리 잡는 핵심!) */ - "strict": true, - "noUncheckedIndexedAccess": true, - - /* 기타 추천 옵션 */ - "skipLibCheck": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "sourceMap": true + /* 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": "es2016" /* 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. */ + // "libReplacement": true, /* Enable lib replacement. */ + "experimentalDecorators": true, /* Enable experimental support for legacy experimental 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": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* 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. */ + "rootDir": "src" /* Allow multiple folders to be treated as one when resolving modules. */, + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "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. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''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. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "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": "dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "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. */ + // "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. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "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. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "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. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] // 빌드 결과물(dist)도 검사에서 제외하는 게 좋아요! -} \ No newline at end of file + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..804be4b --- /dev/null +++ b/tsoa.json @@ -0,0 +1,12 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*.controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3 + }, + "routes": { + "routesDir": "src/generated" + } +} \ No newline at end of file