diff --git a/.gitignore b/.gitignore index 5d0d55fc..0ea1f643 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ node_modules/ *.key *.crt robots.txt -sitemap.xml \ No newline at end of file +sitemap.xml +sitemap-*.xml diff --git a/.pnp.cjs b/.pnp.cjs index 12c3aac7..19c2c56c 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -8690,6 +8690,7 @@ const RAW_RUNTIME_STATE = ["react-chartjs-2", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:5.3.0"],\ ["react-dom", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:19.1.0"],\ ["react-hook-form", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:7.59.0"],\ + ["react-pdf", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:9.2.1"],\ ["tailwindcss", "npm:4.1.11"],\ ["three", "npm:0.177.0"],\ ["ts-node", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:10.9.2"],\ @@ -18090,6 +18091,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["canvas", [\ + ["npm:3.2.1", {\ + "packageLocation": "./.yarn/unplugged/canvas-npm-3.2.1-b6952492b7/node_modules/canvas/",\ + "packageDependencies": [\ + ["canvas", "npm:3.2.1"],\ + ["node-addon-api", "npm:7.1.1"],\ + ["node-gyp", "npm:11.2.0"],\ + ["prebuild-install", "npm:7.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["chai", [\ ["npm:5.2.0", {\ "packageLocation": "./.yarn/cache/chai-npm-5.2.0-373e52d821-dfd1cb719c.zip/node_modules/chai/",\ @@ -18216,6 +18229,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["chownr", [\ + ["npm:1.1.4", {\ + "packageLocation": "./.yarn/cache/chownr-npm-1.1.4-5bd400ab08-ed57952a84.zip/node_modules/chownr/",\ + "packageDependencies": [\ + ["chownr", "npm:1.1.4"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.0.0", {\ "packageLocation": "./.yarn/cache/chownr-npm-3.0.0-5275e85d25-43925b8770.zip/node_modules/chownr/",\ "packageDependencies": [\ @@ -19145,6 +19165,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["decompress-response", [\ + ["npm:6.0.0", {\ + "packageLocation": "./.yarn/cache/decompress-response-npm-6.0.0-359de2878c-bd89d23141.zip/node_modules/decompress-response/",\ + "packageDependencies": [\ + ["decompress-response", "npm:6.0.0"],\ + ["mimic-response", "npm:3.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["dedent", [\ ["npm:1.6.0", {\ "packageLocation": "./.yarn/cache/dedent-npm-1.6.0-2a2b4ba2b1-671b8f5e39.zip/node_modules/dedent/",\ @@ -19176,6 +19206,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["deep-extend", [\ + ["npm:0.6.0", {\ + "packageLocation": "./.yarn/cache/deep-extend-npm-0.6.0-e182924219-1c6b0abcdb.zip/node_modules/deep-extend/",\ + "packageDependencies": [\ + ["deep-extend", "npm:0.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["deep-is", [\ ["npm:0.1.4", {\ "packageLocation": "./.yarn/cache/deep-is-npm-0.1.4-88938b5a67-7f0ee496e0.zip/node_modules/deep-is/",\ @@ -19311,6 +19350,13 @@ const RAW_RUNTIME_STATE = ["detect-libc", "npm:2.0.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.1.2", {\ + "packageLocation": "./.yarn/cache/detect-libc-npm-2.1.2-d0c382b1e2-acc675c29a.zip/node_modules/detect-libc/",\ + "packageDependencies": [\ + ["detect-libc", "npm:2.1.2"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["detect-newline", [\ @@ -20614,6 +20660,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["expand-template", [\ + ["npm:2.0.3", {\ + "packageLocation": "./.yarn/cache/expand-template-npm-2.0.3-80de959306-1c9e7afe9a.zip/node_modules/expand-template/",\ + "packageDependencies": [\ + ["expand-template", "npm:2.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["expect", [\ ["npm:30.0.3", {\ "packageLocation": "./.yarn/cache/expect-npm-30.0.3-b2a8f9af08-6bb88a42d6.zip/node_modules/expect/",\ @@ -21292,6 +21347,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fs-constants", [\ + ["npm:1.0.0", {\ + "packageLocation": "./.yarn/cache/fs-constants-npm-1.0.0-59576b2177-a0cde99085.zip/node_modules/fs-constants/",\ + "packageDependencies": [\ + ["fs-constants", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fs-extra", [\ ["npm:10.1.0", {\ "packageLocation": "./.yarn/cache/fs-extra-npm-10.1.0-86573680ed-5f579466e7.zip/node_modules/fs-extra/",\ @@ -21493,6 +21557,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["github-from-package", [\ + ["npm:0.0.0", {\ + "packageLocation": "./.yarn/cache/github-from-package-npm-0.0.0-519f80c9a1-737ee3f52d.zip/node_modules/github-from-package/",\ + "packageDependencies": [\ + ["github-from-package", "npm:0.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["glob", [\ ["npm:10.4.5", {\ "packageLocation": "./.yarn/cache/glob-npm-10.4.5-8c63175f05-19a9759ea7.zip/node_modules/glob/",\ @@ -22131,6 +22204,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["ini", [\ + ["npm:1.3.8", {\ + "packageLocation": "./.yarn/cache/ini-npm-1.3.8-fb5040b4c0-ec93838d23.zip/node_modules/ini/",\ + "packageDependencies": [\ + ["ini", "npm:1.3.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["inquirer", [\ ["npm:6.5.2", {\ "packageLocation": "./.yarn/cache/inquirer-npm-6.5.2-4f6408c247-a5aa53a8f8.zip/node_modules/inquirer/",\ @@ -25391,6 +25473,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["make-cancellable-promise", [\ + ["npm:1.3.2", {\ + "packageLocation": "./.yarn/cache/make-cancellable-promise-npm-1.3.2-6612d27c4e-10aa0450c7.zip/node_modules/make-cancellable-promise/",\ + "packageDependencies": [\ + ["make-cancellable-promise", "npm:1.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["make-dir", [\ ["npm:3.1.0", {\ "packageLocation": "./.yarn/cache/make-dir-npm-3.1.0-d1d7505142-56aaafefc4.zip/node_modules/make-dir/",\ @@ -25418,6 +25509,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["make-event-props", [\ + ["npm:1.6.2", {\ + "packageLocation": "./.yarn/cache/make-event-props-npm-1.6.2-89d60d5202-ecf0b742e4.zip/node_modules/make-event-props/",\ + "packageDependencies": [\ + ["make-event-props", "npm:1.6.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["make-fetch-happen", [\ ["npm:14.0.3", {\ "packageLocation": "./.yarn/cache/make-fetch-happen-npm-14.0.3-23b30e8691-c40efb5e52.zip/node_modules/make-fetch-happen/",\ @@ -25536,6 +25636,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["merge-refs", [\ + ["npm:1.3.0", {\ + "packageLocation": "./.yarn/cache/merge-refs-npm-1.3.0-3b965c4c45-403d20d283.zip/node_modules/merge-refs/",\ + "packageDependencies": [\ + ["merge-refs", "npm:1.3.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:a4e997d1238027b6f64c1eb66438b0f71064cabb137a4a0d099c7a75ff9f332487bec2fb27637d58df006dcf5422d5deaee216de4f09141ad8bb94929b4c6a68#npm:1.3.0", {\ + "packageLocation": "./.yarn/__virtual__/merge-refs-virtual-d21ed373bc/0/cache/merge-refs-npm-1.3.0-3b965c4c45-403d20d283.zip/node_modules/merge-refs/",\ + "packageDependencies": [\ + ["@types/react", "npm:19.1.8"],\ + ["merge-refs", "virtual:a4e997d1238027b6f64c1eb66438b0f71064cabb137a4a0d099c7a75ff9f332487bec2fb27637d58df006dcf5422d5deaee216de4f09141ad8bb94929b4c6a68#npm:1.3.0"]\ + ],\ + "packagePeers": [\ + "@types/react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["merge-stream", [\ ["npm:2.0.0", {\ "packageLocation": "./.yarn/cache/merge-stream-npm-2.0.0-2ac83efea5-867fdbb30a.zip/node_modules/merge-stream/",\ @@ -25680,6 +25800,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["mimic-response", [\ + ["npm:3.1.0", {\ + "packageLocation": "./.yarn/cache/mimic-response-npm-3.1.0-a4a24b4e96-0d6f07ce6e.zip/node_modules/mimic-response/",\ + "packageDependencies": [\ + ["mimic-response", "npm:3.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["min-indent", [\ ["npm:1.0.1", {\ "packageLocation": "./.yarn/cache/min-indent-npm-1.0.1-77031f50e1-7e207bd5c2.zip/node_modules/min-indent/",\ @@ -25845,6 +25974,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["mkdirp-classic", [\ + ["npm:0.5.3", {\ + "packageLocation": "./.yarn/cache/mkdirp-classic-npm-0.5.3-3b5c991910-95371d831d.zip/node_modules/mkdirp-classic/",\ + "packageDependencies": [\ + ["mkdirp-classic", "npm:0.5.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["module-details-from-path", [\ ["npm:1.0.4", {\ "packageLocation": "./.yarn/cache/module-details-from-path-npm-1.0.4-c3d0545459-10863413e9.zip/node_modules/module-details-from-path/",\ @@ -26039,6 +26177,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["napi-build-utils", [\ + ["npm:2.0.0", {\ + "packageLocation": "./.yarn/cache/napi-build-utils-npm-2.0.0-95da9c2e4e-5833aaeb5c.zip/node_modules/napi-build-utils/",\ + "packageDependencies": [\ + ["napi-build-utils", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["napi-postinstall", [\ ["npm:0.2.5", {\ "packageLocation": "./.yarn/cache/napi-postinstall-npm-0.2.5-2d85d6ee0e-c4a1a8ca61.zip/node_modules/napi-postinstall/",\ @@ -26194,6 +26341,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["node-abi", [\ + ["npm:3.87.0", {\ + "packageLocation": "./.yarn/cache/node-abi-npm-3.87.0-502d02db75-41cfc361ed.zip/node_modules/node-abi/",\ + "packageDependencies": [\ + ["node-abi", "npm:3.87.0"],\ + ["semver", "npm:7.7.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["node-abort-controller", [\ ["npm:3.1.1", {\ "packageLocation": "./.yarn/cache/node-abort-controller-npm-3.1.1-e246ed42cd-f7ad0e7a8e.zip/node_modules/node-abort-controller/",\ @@ -26203,6 +26360,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["node-addon-api", [\ + ["npm:7.1.1", {\ + "packageLocation": "./.yarn/unplugged/node-addon-api-npm-7.1.1-bfb302df19/node_modules/node-addon-api/",\ + "packageDependencies": [\ + ["node-addon-api", "npm:7.1.1"],\ + ["node-gyp", "npm:11.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["node-emoji", [\ ["npm:1.11.0", {\ "packageLocation": "./.yarn/cache/node-emoji-npm-1.11.0-dd2f09050c-5dac6502db.zip/node_modules/node-emoji/",\ @@ -26812,6 +26979,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["path2d", [\ + ["npm:0.2.2", {\ + "packageLocation": "./.yarn/cache/path2d-npm-0.2.2-61baf92922-1bb76c7f27.zip/node_modules/path2d/",\ + "packageDependencies": [\ + ["path2d", "npm:0.2.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["pathe", [\ ["npm:2.0.3", {\ "packageLocation": "./.yarn/cache/pathe-npm-2.0.3-0924246ee0-c118dc5a8b.zip/node_modules/pathe/",\ @@ -26830,6 +27006,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["pdfjs-dist", [\ + ["npm:4.8.69", {\ + "packageLocation": "./.yarn/cache/pdfjs-dist-npm-4.8.69-789befba80-dc297f2a36.zip/node_modules/pdfjs-dist/",\ + "packageDependencies": [\ + ["canvas", "npm:3.2.1"],\ + ["path2d", "npm:0.2.2"],\ + ["pdfjs-dist", "npm:4.8.69"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["pend", [\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/pend-npm-1.2.0-7a13d93266-8a87e63f7a.zip/node_modules/pend/",\ @@ -27086,6 +27273,27 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["prebuild-install", [\ + ["npm:7.1.3", {\ + "packageLocation": "./.yarn/cache/prebuild-install-npm-7.1.3-8e79c3a0a2-25919a42b5.zip/node_modules/prebuild-install/",\ + "packageDependencies": [\ + ["detect-libc", "npm:2.1.2"],\ + ["expand-template", "npm:2.0.3"],\ + ["github-from-package", "npm:0.0.0"],\ + ["minimist", "npm:1.2.8"],\ + ["mkdirp-classic", "npm:0.5.3"],\ + ["napi-build-utils", "npm:2.0.0"],\ + ["node-abi", "npm:3.87.0"],\ + ["prebuild-install", "npm:7.1.3"],\ + ["pump", "npm:3.0.3"],\ + ["rc", "npm:1.2.8"],\ + ["simple-get", "npm:4.0.1"],\ + ["tar-fs", "npm:2.1.4"],\ + ["tunnel-agent", "npm:0.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["prelude-ls", [\ ["npm:1.2.1", {\ "packageLocation": "./.yarn/cache/prelude-ls-npm-1.2.1-3e4d272a55-b00d617431.zip/node_modules/prelude-ls/",\ @@ -27386,6 +27594,19 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["rc", [\ + ["npm:1.2.8", {\ + "packageLocation": "./.yarn/cache/rc-npm-1.2.8-d6768ac936-24a0765315.zip/node_modules/rc/",\ + "packageDependencies": [\ + ["deep-extend", "npm:0.6.0"],\ + ["ini", "npm:1.3.8"],\ + ["minimist", "npm:1.2.8"],\ + ["rc", "npm:1.2.8"],\ + ["strip-json-comments", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react", [\ ["npm:19.1.0", {\ "packageLocation": "./.yarn/cache/react-npm-19.1.0-9804a7da5b-530fb9a622.zip/node_modules/react/",\ @@ -27564,6 +27785,40 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-pdf", [\ + ["npm:9.2.1", {\ + "packageLocation": "./.yarn/cache/react-pdf-npm-9.2.1-a56cdc884b-69b5456b39.zip/node_modules/react-pdf/",\ + "packageDependencies": [\ + ["react-pdf", "npm:9.2.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:9.2.1", {\ + "packageLocation": "./.yarn/__virtual__/react-pdf-virtual-a4e997d123/0/cache/react-pdf-npm-9.2.1-a56cdc884b-69b5456b39.zip/node_modules/react-pdf/",\ + "packageDependencies": [\ + ["@types/react", "npm:19.1.8"],\ + ["@types/react-dom", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:19.1.6"],\ + ["clsx", "npm:2.1.1"],\ + ["dequal", "npm:2.0.3"],\ + ["make-cancellable-promise", "npm:1.3.2"],\ + ["make-event-props", "npm:1.6.2"],\ + ["merge-refs", "virtual:a4e997d1238027b6f64c1eb66438b0f71064cabb137a4a0d099c7a75ff9f332487bec2fb27637d58df006dcf5422d5deaee216de4f09141ad8bb94929b4c6a68#npm:1.3.0"],\ + ["pdfjs-dist", "npm:4.8.69"],\ + ["react", "npm:19.1.0"],\ + ["react-dom", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:19.1.0"],\ + ["react-pdf", "virtual:a21e24cf4a793f04e2e07474217597c8b8491086897b7ceb58f7c959b6c65ef5decbff14fa97445a76a680e4e81febb25c198102b2ba038b85eb731f05365753#npm:9.2.1"],\ + ["tiny-invariant", "npm:1.3.3"],\ + ["warning", "npm:4.0.3"]\ + ],\ + "packagePeers": [\ + "@types/react-dom",\ + "@types/react",\ + "react-dom",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-reconciler", [\ ["npm:0.31.0", {\ "packageLocation": "./.yarn/cache/react-reconciler-npm-0.31.0-8f5bda4868-97920e1866.zip/node_modules/react-reconciler/",\ @@ -28614,6 +28869,27 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["simple-concat", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/simple-concat-npm-1.0.1-48df70de29-62f7508e67.zip/node_modules/simple-concat/",\ + "packageDependencies": [\ + ["simple-concat", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["simple-get", [\ + ["npm:4.0.1", {\ + "packageLocation": "./.yarn/cache/simple-get-npm-4.0.1-fa2a97645d-b0649a581d.zip/node_modules/simple-get/",\ + "packageDependencies": [\ + ["decompress-response", "npm:6.0.0"],\ + ["once", "npm:1.4.0"],\ + ["simple-concat", "npm:1.0.1"],\ + ["simple-get", "npm:4.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["simple-swizzle", [\ ["npm:0.2.2", {\ "packageLocation": "./.yarn/cache/simple-swizzle-npm-0.2.2-8dee37fad1-df5e4662a8.zip/node_modules/simple-swizzle/",\ @@ -29173,6 +29449,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["strip-json-comments", [\ + ["npm:2.0.1", {\ + "packageLocation": "./.yarn/cache/strip-json-comments-npm-2.0.1-e7883b2d04-b509231cbd.zip/node_modules/strip-json-comments/",\ + "packageDependencies": [\ + ["strip-json-comments", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.1.1", {\ "packageLocation": "./.yarn/cache/strip-json-comments-npm-3.1.1-dcb2324823-9681a6257b.zip/node_modules/strip-json-comments/",\ "packageDependencies": [\ @@ -29449,6 +29732,17 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["tar-fs", [\ + ["npm:2.1.4", {\ + "packageLocation": "./.yarn/cache/tar-fs-npm-2.1.4-90a454735f-decb25acdc.zip/node_modules/tar-fs/",\ + "packageDependencies": [\ + ["chownr", "npm:1.1.4"],\ + ["mkdirp-classic", "npm:0.5.3"],\ + ["pump", "npm:3.0.3"],\ + ["tar-fs", "npm:2.1.4"],\ + ["tar-stream", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.1.0", {\ "packageLocation": "./.yarn/cache/tar-fs-npm-3.1.0-e568911671-7603096775.zip/node_modules/tar-fs/",\ "packageDependencies": [\ @@ -29462,6 +29756,18 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["tar-stream", [\ + ["npm:2.2.0", {\ + "packageLocation": "./.yarn/cache/tar-stream-npm-2.2.0-884c79b510-2f4c910b3e.zip/node_modules/tar-stream/",\ + "packageDependencies": [\ + ["bl", "npm:4.1.0"],\ + ["end-of-stream", "npm:1.4.5"],\ + ["fs-constants", "npm:1.0.0"],\ + ["inherits", "npm:2.0.4"],\ + ["readable-stream", "npm:3.6.2"],\ + ["tar-stream", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.1.7", {\ "packageLocation": "./.yarn/cache/tar-stream-npm-3.1.7-c34f9aa00f-a09199d21f.zip/node_modules/tar-stream/",\ "packageDependencies": [\ @@ -30277,6 +30583,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["tunnel-agent", [\ + ["npm:0.6.0", {\ + "packageLocation": "./.yarn/cache/tunnel-agent-npm-0.6.0-64345ab7eb-4c7a1b813e.zip/node_modules/tunnel-agent/",\ + "packageDependencies": [\ + ["safe-buffer", "npm:5.2.1"],\ + ["tunnel-agent", "npm:0.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tunnel-rat", [\ ["npm:0.1.2", {\ "packageLocation": "./.yarn/cache/tunnel-rat-npm-0.1.2-69bf8f367e-93cd50c7c9.zip/node_modules/tunnel-rat/",\ @@ -31428,6 +31744,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["warning", [\ + ["npm:4.0.3", {\ + "packageLocation": "./.yarn/cache/warning-npm-4.0.3-291e921d6d-aebab44512.zip/node_modules/warning/",\ + "packageDependencies": [\ + ["loose-envify", "npm:1.4.0"],\ + ["warning", "npm:4.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["watchpack", [\ ["npm:2.4.4", {\ "packageLocation": "./.yarn/cache/watchpack-npm-2.4.4-01f92bffc4-6c0901f75c.zip/node_modules/watchpack/",\ diff --git a/apps/client/next.config.ts b/apps/client/next.config.ts index 12fc019b..421ad2f9 100644 --- a/apps/client/next.config.ts +++ b/apps/client/next.config.ts @@ -34,6 +34,17 @@ const nextConfig: NextConfig = { webpackBuildWorker: true, preloadEntriesOnStart: true, urlImports: ["https://cdn.jsdelivr.net/"] + }, + webpack: (config) => { + config.resolve.alias = { + ...config.resolve.alias, + canvas: false + }; + config.resolve.fallback = { + ...config.resolve.fallback, + canvas: false + }; + return config } }; diff --git a/apps/client/package.json b/apps/client/package.json index ec9b1be4..a64fdb6b 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -35,6 +35,7 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-hook-form": "^7.59.0", + "react-pdf": "^9.2.1", "zod": "^3.25.74" }, "devDependencies": { diff --git a/apps/client/public/og/resume-interview.png b/apps/client/public/og/resume-interview.png new file mode 100644 index 00000000..0c2c7493 Binary files /dev/null and b/apps/client/public/og/resume-interview.png differ diff --git a/apps/client/src/domains/dashboard/components/selectSection.tsx b/apps/client/src/domains/dashboard/components/selectSection.tsx index 266f86fa..5f16ba44 100644 --- a/apps/client/src/domains/dashboard/components/selectSection.tsx +++ b/apps/client/src/domains/dashboard/components/selectSection.tsx @@ -2,11 +2,20 @@ import { useState } from "react"; import InterviewHistory from "./interviewHistory"; import ChangeNickname from "@/domains/dashboard/components/changeNickname"; import Withdrawal from "@/domains/dashboard/components/withDrawl"; +import ResumeBasedInterviewHistory from "@/domains/resume/components/resumeBasedInterviewHistory"; +import ResumeEvaluationHistory from "@/domains/resume/components/resumeEvaluationHistory"; +import ArchivePreview from "@/domains/resume/components/archivePreview"; import { UserInfo } from "@kokomen/types"; import { Button } from "@kokomen/ui"; import { useRouter } from "next/router"; -type Section = "interview" | "changeNickname" | "withdrawal"; +type Section = + | "interview" + | "resumeBasedInterview" + | "resumeEvaluation" + | "archive" + | "changeNickname" + | "withdrawal"; interface SelectSectionProps { userInfo: UserInfo; @@ -16,6 +25,21 @@ const interviewSections: { label: string; value: Section }[] = [ { label: "면접 기록", value: "interview" + }, + { + label: "이력서 기반 면접 질문", + value: "resumeBasedInterview" + }, + { + label: "이력서 평가 결과", + value: "resumeEvaluation" + } +]; + +const archiveSections: { label: string; value: Section }[] = [ + { + label: "아카이브", + value: "archive" } ]; @@ -62,6 +86,23 @@ export default function SelectSection({ userInfo }: SelectSectionProps) { ))}
+

아카이브

+ {archiveSections.map((sec) => ( + + ))} +
+

유저 정보 관리

@@ -121,6 +162,12 @@ function SelectedSection({ switch (section) { case "interview": return ; + case "resumeBasedInterview": + return ; + case "resumeEvaluation": + return ; + case "archive": + return ; case "changeNickname": return ; case "withdrawal": diff --git a/apps/client/src/domains/resume/api/index.ts b/apps/client/src/domains/resume/api/index.ts index 3ef35182..f4f37f93 100644 --- a/apps/client/src/domains/resume/api/index.ts +++ b/apps/client/src/domains/resume/api/index.ts @@ -3,7 +3,9 @@ import { ResumeEvaluationResult, ResumeFailed, ResumeOutput, - ResumePending + ResumePending, + ResumeEvaluationsResponse, + CamelCasedProperties } from "@kokomen/types"; import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; import { delay, exponentialDelay } from "@kokomen/utils"; @@ -147,9 +149,38 @@ function getResumeEvaluationResult( .then((res) => res.data) .then(mapToCamelCase); } + +function getResumeEvaluations( + page: number = 0, + size: number = 20, + context?: GetServerSidePropsContext +): Promise> { + const instance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL + "/resumes/evaluations", + withCredentials: true + }); + + return instance + .get>("", { + params: { + page, + size + }, + headers: context + ? { + Cookie: context.req.headers.cookie + } + : undefined + }) + .then((res) => res.data) + .then(mapToCamelCase); +} + export { submitResumeEvaluation, getResumeEvaluationState, - getResumeEvaluationResult + getResumeEvaluationResult, + getResumeEvaluations }; export * from "./archive"; +export * from "./resumeBasedInterview"; diff --git a/apps/client/src/domains/resume/api/resumeBasedInterview.ts b/apps/client/src/domains/resume/api/resumeBasedInterview.ts new file mode 100644 index 00000000..e9fb733c --- /dev/null +++ b/apps/client/src/domains/resume/api/resumeBasedInterview.ts @@ -0,0 +1,230 @@ +import { mapToCamelCase } from "@/utils/convertConvention"; +import { + ResumeInterviewPending, + ResumeInterviewFailed, + ResumeBasedInterviewQuestion, + ResumeInterviewSuccess, + Interview, + InterviewMode, + ResumeBasedInterviewGenerationsResponse, + CamelCasedProperties +} from "@kokomen/types"; +import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; +import { delay, exponentialDelay } from "@kokomen/utils"; +import { GetServerSidePropsContext } from "next"; + +// 요청별 재시도 상태를 관리하는 Map +const retryStateMap = new Map(); + +// 요청 식별자 생성 함수 +const createRequestId = (config: AxiosRequestConfig): string => { + const { method, url, data } = config; + return `${method}:${url}:${JSON.stringify(data || {})}`; +}; + +// 재시도 상태 관리 함수들 +const getRetryCount = (requestId: string): number => { + return retryStateMap.get(requestId) || 0; +}; + +const incrementRetryCount = (requestId: string): number => { + const currentCount = getRetryCount(requestId); + const newCount = currentCount + 1; + retryStateMap.set(requestId, newCount); + return newCount; +}; + +const resetRetryCount = (requestId: string): void => { + retryStateMap.delete(requestId); +}; + +// 이력서 제출 부분 +const resumeBasedInterviewServerInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL + "/interviews/resume-based", + withCredentials: true +}); +resumeBasedInterviewServerInstance.interceptors.response.use( + (response: AxiosResponse) => { + // 성공 시 해당 요청의 retry 상태 정리 + const requestId = createRequestId(response.config); + resetRetryCount(requestId); + return response; + }, + + // 에러 응답 처리 + async (error: AxiosError) => { + const requestId = createRequestId(error.config as AxiosRequestConfig); + const retryCount = incrementRetryCount(requestId); + const maxRetries = 3; + + if (retryCount >= maxRetries) { + resetRetryCount(requestId); + return Promise.reject(error); + } + + await exponentialDelay(retryCount); + return resumeBasedInterviewServerInstance.request( + error.config as AxiosRequestConfig + ); + } +); + +function generateResumeBasedInterviewQuestion(data: FormData) { + return resumeBasedInterviewServerInstance + .post<{ resume_based_interview_result_id: number }>( + "/questions/generate", + data + ) + .then((res) => res.data) + .then(mapToCamelCase); +} + +const resumeBasedInterviewQuestionPollingServerInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL + "/interviews/resume-based", + withCredentials: true +}); + +// 폴링에 대한 요청 완료시 +const onFullFilledPolling = async ( + response: AxiosResponse< + ResumeInterviewSuccess | ResumeInterviewPending | ResumeInterviewFailed + > +) => { + const requestId = createRequestId(response.config); + console.log(response.data); + + if (response.data.state === "COMPLETED") { + resetRetryCount(requestId); + return response; + } + + if (response.data.state === "FAILED") + return Promise.reject("이력서 평가 중 오류가 발생했어요"); + + const retryCount = incrementRetryCount(requestId); + const maxRetries = 50; + + if (retryCount >= maxRetries) { + resetRetryCount(requestId); + return Promise.reject("서버가 응답하지 않습니다."); + } + + await delay(1000); + return resumeBasedInterviewQuestionPollingServerInstance.request( + response.config + ); +}; + +// 폴링 에러 처리 함수 +const onRejectedPolling = async (error: AxiosError) => { + const requestId = createRequestId(error.config as AxiosRequestConfig); + const retryCount = incrementRetryCount(requestId); + const maxRetries = 3; + + if (retryCount >= maxRetries) { + resetRetryCount(requestId); + return Promise.reject(error); + } + + await exponentialDelay(retryCount); + return resumeBasedInterviewQuestionPollingServerInstance.request( + error.config as AxiosRequestConfig + ); +}; + +// 인터뷰 면접 답변 폴링을 위한 서버 인스턴스 +resumeBasedInterviewQuestionPollingServerInstance.interceptors.response.use( + onFullFilledPolling, + onRejectedPolling +); + +function checkResumeBasedInterviewQuestion( + resumeBasedInterviewQuestionId: number +) { + return resumeBasedInterviewQuestionPollingServerInstance + .get(`/${resumeBasedInterviewQuestionId}/check`) + .then((res) => res.data) + .then(mapToCamelCase); +} + +const resumeBasedInterviewResultServerInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL + "/interviews/resume-based", + withCredentials: true +}); + +function getResumeInterviewResult( + resumeBasedInterviewResultId: number, + context: GetServerSidePropsContext +) { + return resumeBasedInterviewResultServerInstance + .get(`/${resumeBasedInterviewResultId}`, { + headers: { + Cookie: context.req.headers.cookie + } + }) + .then((res) => res.data) + .then(mapToCamelCase); +} + +function getResumeBasedInterviewGenerations( + page: number = 0, + context?: GetServerSidePropsContext +): Promise> { + const instance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL + "/interviews/resume-based", + withCredentials: true + }); + + return instance + .get>( + "/questions/generations", + { + params: { + page + }, + headers: context + ? { + Cookie: context.req.headers.cookie + } + : undefined + } + ) + .then((res) => res.data) + .then(mapToCamelCase); +} + +const resumeBasedInterviewCreateInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, + withCredentials: true +}); + +function createResumeBasedInterview({ + resumeBasedInterviewResultId, + generatedQuestionId, + maxQuestionCount, + mode +}: { + resumeBasedInterviewResultId: number; + generatedQuestionId: number; + maxQuestionCount: number; + mode: InterviewMode; +}): Promise { + return resumeBasedInterviewCreateInstance + .post( + `/interviews/resume-based/${resumeBasedInterviewResultId}`, + { + generated_question_id: generatedQuestionId, + max_question_count: maxQuestionCount, + mode + } + ) + .then((res) => res.data); +} + +export { + generateResumeBasedInterviewQuestion, + checkResumeBasedInterviewQuestion, + getResumeInterviewResult, + createResumeBasedInterview, + getResumeBasedInterviewGenerations +}; diff --git a/apps/client/src/domains/resume/components/archivePreview.tsx b/apps/client/src/domains/resume/components/archivePreview.tsx new file mode 100644 index 00000000..89ab4ac6 --- /dev/null +++ b/apps/client/src/domains/resume/components/archivePreview.tsx @@ -0,0 +1,242 @@ +"use client" + +import { getArchivedResumes } from "@/domains/resume/api"; +import { ArchivedResumeAndPortfolio, CamelCasedProperties } from "@kokomen/types"; +import { archiveKeys } from "@/utils/querykeys"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { Button, Modal } from "@kokomen/ui"; +import { FileText, Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; + +// eslint-disable-next-line @rushstack/typedef-var +const PdfViewer = dynamic( + () => import("../components/pdfViewer"), + { + ssr: false, + loading: () =>
+ +
+ } +); + + +type TabType = "RESUME" | "PORTFOLIO"; + + +export default function ArchivePreview() { + const [activeTab, setActiveTab] = useState("RESUME"); + const [selectedFile, setSelectedFile] = + useState | null>(null); + const [numPages, setNumPages] = useState(); + const [pageNumber, setPageNumber] = useState(1); + const [isModalOpen, setIsModalOpen] = useState(false); + + const { data, isLoading, isError } = useQuery({ + queryKey: archiveKeys.resumes("ALL"), + queryFn: () => getArchivedResumes("ALL"), + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 5 + }); + + const resumes = data?.resumes || []; + const portfolios = data?.portfolios || []; + + const currentList = + activeTab === "RESUME" ? resumes : portfolios; + + const handleFileSelect = ( + file: CamelCasedProperties + ): void => { + setSelectedFile(file); + setPageNumber(1); + setNumPages(undefined); + setIsModalOpen(true); + }; + + const handleCloseModal = (): void => { + setIsModalOpen(false); + setSelectedFile(null); + setPageNumber(1); + setNumPages(undefined); + }; + + const handleDocumentLoadSuccess = ({ numPages }: { numPages: number }): void => { + console.log('numPages', numPages); + setNumPages(numPages); + setPageNumber(1); + }; + + const goToPrevPage = (): void => { + setPageNumber((prev) => Math.max(1, prev - 1)); + }; + + const goToNextPage = (): void => { + setPageNumber((prev) => Math.min(numPages || 0, prev + 1)); + }; + + + if (isLoading) { + return ( +
+
+

+ 아카이브 미리보기 +

+

+ 아카이빙된 이력서와 포트폴리오를 미리보기할 수 있습니다 +

+
+
+ +
+
+ ); + } + + if (isError) { + return ( +
+
+

+ 아카이브 미리보기 +

+

+ 아카이빙된 이력서와 포트폴리오를 미리보기할 수 있습니다 +

+
+
+

+ 데이터를 불러오는 중 오류가 발생했습니다. +

+
+
+ ); + } + + return ( +
+
+

+ 아카이브 미리보기 +

+

+ 아카이빙된 이력서와 포트폴리오를 미리보기할 수 있습니다 +

+
+ +
+ {/* 파일 목록 */} +
+ {/* 탭 */} +
+ + +
+ + {/* 파일 리스트 */} +
+ {currentList.length === 0 ? ( +
+ +

+ {activeTab === "RESUME" + ? "아카이빙된 이력서가 없습니다" + : "아카이빙된 포트폴리오가 없습니다"} +

+
+ ) : ( + currentList.map((file) => ( + + )) + )} +
+
+
+ + {/* PDF 미리보기 모달 */} + {selectedFile && ( + +
+

+ {new Date(selectedFile.createdAt).toLocaleDateString("ko-KR")} +

+
+ +
+
+ +
+ + + {pageNumber} / {numPages || "-"} + + +
+
+ +
+
+ )} +
+ ); +} diff --git a/apps/client/src/domains/resume/components/pdfViewer.tsx b/apps/client/src/domains/resume/components/pdfViewer.tsx new file mode 100644 index 00000000..82964bfd --- /dev/null +++ b/apps/client/src/domains/resume/components/pdfViewer.tsx @@ -0,0 +1,55 @@ +"use client" + +import { Document, Page, pdfjs } from "react-pdf"; +import { JSX } from "react"; +import "react-pdf/dist/Page/TextLayer.css"; +import "react-pdf/dist/Page/AnnotationLayer.css"; + + +// workerSrc 정의 하지 않으면 pdf 보여지지 않습니다. +pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; +const options = { + cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`, + standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts/`, + }; + +type PdfViewerProps = { + fileUrl: string; + pageNumber: number; + // eslint-disable-next-line no-unused-vars + onLoadSuccess: ({ numPages }: { numPages: number }) => void; +}; + +export default function PdfViewer({ + fileUrl, + pageNumber, + onLoadSuccess +}: PdfViewerProps): JSX.Element { + // const [isLoading, setIsLoading] = useState(true); + + return ( + + { + onLoadSuccess({ numPages }); + }} + error={ +
+

PDF를 불러올 수 없습니다.

+

+ 파일이 손상되었거나 지원되지 않는 형식일 수 있습니다. +

+
+ } + > + +
+ ); +} diff --git a/apps/client/src/domains/resume/components/resumeBasedInterviewForm.tsx b/apps/client/src/domains/resume/components/resumeBasedInterviewForm.tsx new file mode 100644 index 00000000..51958b96 --- /dev/null +++ b/apps/client/src/domains/resume/components/resumeBasedInterviewForm.tsx @@ -0,0 +1,225 @@ +import { generateResumeBasedInterviewQuestion } from "@/domains/resume/api"; +import { ArchiveButton } from "@/domains/resume/components/resumeArchiveButton"; +import useExtendedRouter from "@/hooks/useExtendedRouter"; +import { withApiErrorCapture } from "@/utils/error"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; +import { generateFormData } from "@kokomen/utils"; +import { CamelCasedProperties, UserInfo } from "@kokomen/types"; +import { Button, FileField, useToast } from "@kokomen/ui"; +import { useMutation } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { publishResumeBasedInterviewEvent } from "@/domains/resume/utils/resumeInterviewEventEmitter"; + +const jobCareers = ["0-1년", "1-3년", "3-5년", "5-10년", "10년 이상"]; + +// SSR 환경에서 FileList가 정의되지 않을 수 있으므로 custom 검증 사용 +const fileListSchema: z.ZodTypeAny = + typeof FileList !== "undefined" + ? z.instanceof(FileList) + : z.custom((val): val is FileList => { + return typeof FileList !== "undefined" && val instanceof FileList; + }); + +const resumeBasedInterviewFormFields = z + .object({ + // FileList를 직접 받거나, 이미 업로드된 경우를 위해 optional 처리 + resume: fileListSchema.optional(), + resume_id: z.string().optional(), + + portfolio: fileListSchema.optional(), + portfolio_id: z.string().optional(), + + job_career: z.enum(jobCareers as [string, ...string[]]).default("0-1년") + }) + // 1. 이력서 검증: ID가 있거나, 파일이 선택되었거나 + .refine((data) => data.resume_id || (data.resume && data.resume.length > 0), { + message: "이력서를 선택해주세요", + path: ["resume"] // 에러 메시지를 표시할 필드 위치 + }); +type ResumeBasedInterviewFormFields = z.infer< + typeof resumeBasedInterviewFormFields +>; + +export default function ResumeBasedInterviewForm({ user }: { user: UserInfo }) { + const { toast } = useToast(); + const form = useForm({ + resolver: standardSchemaResolver(resumeBasedInterviewFormFields), + defaultValues: { + job_career: "0-1년" + } + }); + const [displayName, setDisplayName] = useState<{ + resume: string; + portfolio: string; + }>({ resume: "", portfolio: "" }); + + useEffect(() => { + const resume = form.getValues("resume"); + const portfolio = form.getValues("portfolio"); + if (resume instanceof FileList && resume.length > 0) { + setDisplayName({ ...displayName, resume: "" }); + form.setValue("resume_id", ""); + } + if (portfolio instanceof FileList && portfolio.length > 0) { + setDisplayName({ ...displayName, portfolio: "" }); + form.setValue("portfolio_id", ""); + } + }, [form.watch("resume_id"), form.watch("portfolio_id")]); + + const router = useExtendedRouter(); + const mutation = useMutation< + CamelCasedProperties<{ resume_based_interview_result_id: number }>, + Error, + FormData + >({ + mutationFn: generateResumeBasedInterviewQuestion, + onSuccess: (data) => { + publishResumeBasedInterviewEvent("resumeBasedInterview:submitted", { + resume_based_interview_result_id: data.resumeBasedInterviewResultId + }); + toast({ + title: "이력서 분석 중입니다. 잠시 후 평가 결과를 알려드려요", + variant: "info" + }); + router.replace({ + pathname: "/resume" + }); + }, + onError: withApiErrorCapture((error) => { + if (isAxiosError(error) && error.response?.status === 401) { + router.navigateToLogin(); + return; + } else { + toast({ + title: "이력서 분석 실패", + description: + "이력서 분석 중 오류가 발생했어요. 잠시 후 다시 시도해주세요", + variant: "error" + }); + } + }) + }); + const [isParsing, setIsParsing] = useState(false); + + async function onSubmit(data: ResumeBasedInterviewFormFields) { + try { + setIsParsing(true); + const formData = generateFormData(data); + mutation.mutate(formData); + } catch (error) { + console.log(error); + } finally { + setIsParsing(false); + } + } + const onclickArchiveButton = (data: { + resume_id?: string; + resume_name?: string; + portfolio_id?: string; + portfolio_name?: string; + }) => { + if (data.resume_id) { + form.setValue("resume_id", data.resume_id); + setDisplayName({ + ...displayName, + resume: data.resume_name || "" + }); + } + if (data.portfolio_id) { + form.setValue("portfolio_id", data.portfolio_id); + setDisplayName({ + ...displayName, + portfolio: data.portfolio_name || "" + }); + } + }; + + const isPending = isParsing || mutation.isPending; + + return ( +
+
+
+

이력서 평가

+

+ 이력서와 포트폴리오를 업로드하고, 지원하려는 직무 정보를 + 입력해주세요. +

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ +
+ {jobCareers.map((career) => ( + + ))} +
+
+
+ +
+
+
+ ); +} diff --git a/apps/client/src/domains/resume/components/resumeBasedInterviewHistory.tsx b/apps/client/src/domains/resume/components/resumeBasedInterviewHistory.tsx new file mode 100644 index 00000000..f581378b --- /dev/null +++ b/apps/client/src/domains/resume/components/resumeBasedInterviewHistory.tsx @@ -0,0 +1,237 @@ +import { getResumeBasedInterviewGenerations } from "@/domains/resume/api/resumeBasedInterview"; +import { ResumeBasedInterviewGenerationsResponse } from "@kokomen/types"; +import { CamelCasedProperties } from "@/utils/convertConvention"; +import { resumeBasedInterviewKeys } from "@/utils/querykeys"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useInfiniteObserver } from "@kokomen/utils"; +import { useRef, JSX } from "react"; +import { + Calendar, + FileText, + ExternalLink, + CheckCircle, + XCircle, + Clock, + Loader2 +} from "lucide-react"; +import Link from "next/link"; +import { formatDate } from "@/utils/date"; + +export default function ResumeBasedInterviewHistory(): JSX.Element { + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isError + } = useInfiniteQuery< + CamelCasedProperties + >({ + queryKey: resumeBasedInterviewKeys.infinite(), + queryFn: ({ pageParam = 0 }) => + getResumeBasedInterviewGenerations(pageParam as number), + getNextPageParam: (lastPage) => { + return lastPage.hasNext ? lastPage.currentPage + 1 : undefined; + }, + initialPageParam: 0 + }); + + const loadMoreRef = useRef(null); + + useInfiniteObserver(loadMoreRef, () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }); + + const getStateBadge = (state: string): JSX.Element | null => { + switch (state) { + case "COMPLETED": + return ( + + + 완료 + + ); + case "PENDING": + return ( + + + 진행중 + + ); + case "FAILED": + return ( + + + 실패 + + ); + default: + return null; + } + }; + + if (isLoading) { + return ( +
+
+

+ 이력서 기반 면접 질문 히스토리 +

+

+ 생성된 이력서 기반 면접 질문들을 확인해보세요 +

+
+
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+
+
+
+ ))} +
+
+ ); + } + + if (isError || !data) { + return ( +
+
+

+ 이력서 기반 면접 질문 히스토리 +

+

+ 생성된 이력서 기반 면접 질문들을 확인해보세요 +

+
+
+

+ 데이터를 불러오는 중 오류가 발생했습니다. +

+
+
+ ); + } + + const generations = data.pages.flatMap((page) => page.data || []); + + return ( +
+
+

+ 이력서 기반 면접 질문 히스토리 +

+

+ 생성된 이력서 기반 면접 질문들을 확인해보세요 +

+
+ + {generations.length === 0 ? ( +
+ +

+ 생성된 이력서 기반 면접 질문이 없습니다 +

+

+ 이력서를 업로드하여 면접 질문을 생성해보세요 +

+
+ ) : ( +
+ {generations.map((generation) => ( +
+
+
+
+

+ 이력서 기반 면접 질문 +

+ {getStateBadge(generation.state)} +
+ +
+
+ + {formatDate(generation.createdAt)} +
+
+ 연차: + {generation.jobCareer} +
+
+ +
+ {generation.resume && ( + + )} + {generation.portfolio && ( +
+ + 포트폴리오: + + {generation.portfolio.name} + + +
+ )} +
+
+ + {generation.state === "COMPLETED" && ( +
+ + 질문 보기 + +
+ )} +
+
+ ))} + + {hasNextPage && ( +
+ {isFetchingNextPage ? ( +
+ + 더 불러오는 중... +
+ ) : ( +
+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/apps/client/src/domains/resume/components/resumeEvaluationForm.tsx b/apps/client/src/domains/resume/components/resumeEvaluationForm.tsx index 8bbca71c..bde47ad4 100644 --- a/apps/client/src/domains/resume/components/resumeEvaluationForm.tsx +++ b/apps/client/src/domains/resume/components/resumeEvaluationForm.tsx @@ -15,13 +15,22 @@ import { archiveKeys } from "@/utils/querykeys"; import { publishReportEvent } from "@/domains/resume/utils/reportEventEmitter"; const jobCareers = ["0-1년", "1-3년", "3-5년", "5-10년", "10년 이상"]; + +// SSR 환경에서 FileList가 정의되지 않을 수 있으므로 custom 검증 사용 +const fileListSchema: z.ZodTypeAny = + typeof FileList !== "undefined" + ? z.instanceof(FileList) + : z.custom((val): val is FileList => { + return typeof FileList !== "undefined" && val instanceof FileList; + }); + const resumeEvalFormFields = z .object({ // FileList를 직접 받거나, 이미 업로드된 경우를 위해 optional 처리 - resume: z.instanceof(FileList).optional(), + resume: fileListSchema.optional(), resume_id: z.string().optional(), - portfolio: z.instanceof(FileList).optional(), + portfolio: fileListSchema.optional(), portfolio_id: z.string().optional(), job_position: z.string().min(1, { message: "지원 직무를 입력해주세요" }), diff --git a/apps/client/src/domains/resume/components/resumeEvaluationHistory.tsx b/apps/client/src/domains/resume/components/resumeEvaluationHistory.tsx new file mode 100644 index 00000000..02d242ad --- /dev/null +++ b/apps/client/src/domains/resume/components/resumeEvaluationHistory.tsx @@ -0,0 +1,209 @@ +import { getResumeEvaluations } from "@/domains/resume/api"; +import { ResumeEvaluationsResponse } from "@kokomen/types"; +import { CamelCasedProperties } from "@/utils/convertConvention"; +import { resumeEvaluationKeys } from "@/utils/querykeys"; +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "next/router"; +import { + Calendar, + Briefcase, + TrendingUp, + CheckCircle, + XCircle, + Clock +} from "lucide-react"; +import Link from "next/link"; +import PaginationButtons from "@/shared/paginationButtons"; +import { formatDate } from "@/utils/date"; + +export default function ResumeEvaluationHistory() { + const router = useRouter(); + const page = Number(router.query.page) || 0; + const size = 20; + + const { data, isLoading, isError } = useQuery< + CamelCasedProperties + >({ + queryKey: resumeEvaluationKeys.history(page, size), + queryFn: () => getResumeEvaluations(page, size) + }); + + const getStateBadge = (state: string) => { + switch (state) { + case "COMPLETED": + return ( + + + 완료 + + ); + case "PENDING": + return ( + + + 진행중 + + ); + case "FAILED": + return ( + + + 실패 + + ); + default: + return null; + } + }; + + const getScoreColor = (score: number) => { + if (score >= 80) return "text-green-600"; + if (score >= 60) return "text-yellow-600"; + return "text-red-600"; + }; + + if (isLoading) { + return ( +
+
+

+ 이력서 평가 히스토리 +

+

+ 이력서 평가 결과를 확인해보세요 +

+
+
+ {Array.from({ length: 5 }).map((_, index) => ( +
+
+
+
+
+ ))} +
+
+ ); + } + + if (isError || !data) { + return ( +
+
+

+ 이력서 평가 히스토리 +

+

+ 이력서 평가 결과를 확인해보세요 +

+
+
+

+ 데이터를 불러오는 중 오류가 발생했습니다. +

+
+
+ ); + } + + const evaluations = data.evaluations || []; + + return ( +
+
+

+ 이력서 평가 히스토리 +

+

+ 이력서 평가 결과를 확인해보세요 +

+
+ + {evaluations.length === 0 ? ( +
+ +

+ 평가한 이력서가 없습니다 +

+

+ 이력서를 업로드하여 평가를 받아보세요 +

+
+ ) : ( +
+ {evaluations.map((evaluation) => ( +
+
+
+
+

+ 이력서 평가 +

+ {getStateBadge(evaluation.state)} +
+ +
+
+ + {formatDate(evaluation.createdAt)} +
+
+ + 직무: + {evaluation.jobPosition} +
+
+ 연차: + {evaluation.jobCareer} +
+
+ + {evaluation.state === "COMPLETED" && ( +
+ + + 총점: {evaluation.totalScore}점 + +
+ )} +
+ + {evaluation.state === "COMPLETED" && ( +
+ + 결과 보기 + +
+ )} +
+
+ ))} + + {data.totalPages > 1 && ( + + )} +
+ )} +
+ ); +} + diff --git a/apps/client/src/domains/resume/components/resumeInterviewModeSelectModal.tsx b/apps/client/src/domains/resume/components/resumeInterviewModeSelectModal.tsx new file mode 100644 index 00000000..b03066f1 --- /dev/null +++ b/apps/client/src/domains/resume/components/resumeInterviewModeSelectModal.tsx @@ -0,0 +1,69 @@ +import { Modal } from "@kokomen/ui"; +import { InterviewMode } from "@kokomen/types"; +import { Mic, FileText } from "lucide-react"; + +interface ResumeInterviewModeSelectModalProps { + isOpen: boolean; + onClose: () => void; + // eslint-disable-next-line no-unused-vars + onSelectMode: (mode: InterviewMode) => void; +} + +export default function ResumeInterviewModeSelectModal({ + isOpen, + onClose, + onSelectMode +}: ResumeInterviewModeSelectModalProps) { + const handleSelectMode = (mode: InterviewMode) => { + onSelectMode(mode); + onClose(); + }; + + return ( + +
+

+ 면접을 진행할 모드를 선택해주세요. +

+ +
+ + + +
+
+
+ ); +} diff --git a/apps/client/src/domains/resume/components/resumeSelectMenuNormal.tsx b/apps/client/src/domains/resume/components/resumeSelectMenuNormal.tsx index 50be9f80..f397135b 100644 --- a/apps/client/src/domains/resume/components/resumeSelectMenuNormal.tsx +++ b/apps/client/src/domains/resume/components/resumeSelectMenuNormal.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; import Image from "next/image"; -import { Tooltip } from "@kokomen/ui"; interface MenuItemProps { title: string; description: string; @@ -109,18 +108,21 @@ export default function ResumeSelectMenuNormal() { 체험해보기
- +
- - 곧 출시돼요! 조금만 기다려주세요 :) - - + + 체험해보기 + +
diff --git a/apps/client/src/domains/resume/context/resumeBasedInterviewStore.tsx b/apps/client/src/domains/resume/context/resumeBasedInterviewStore.tsx new file mode 100644 index 00000000..1b4789bb --- /dev/null +++ b/apps/client/src/domains/resume/context/resumeBasedInterviewStore.tsx @@ -0,0 +1,119 @@ +import { checkResumeBasedInterviewQuestion } from "@/domains/resume/api/resumeBasedInterview"; +import { useResumeBasedInterviewEvent } from "@/domains/resume/utils/resumeInterviewEventEmitter"; +import { RoundSpinner, Tooltip } from "@kokomen/ui"; +import { CheckIcon, X } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import Link from "next/link"; +import React, { createContext, useState } from "react"; + +type ResumeBasedInterviewState = "IDLE" | "PENDING" | "COMPLETED" | "ERROR"; +interface IResumeBasedInterviewStore { + interviewState: ResumeBasedInterviewState; + interviewResultId: number | null; + // eslint-disable-next-line no-unused-vars + setInterviewResultId: (interviewResultId: number) => void; +} +const ResumeBasedInterviewStore = + createContext(null); + +export default function ResumeBasedInterviewStoreProvider({ + children +}: { + children: React.ReactNode; +}) { + const [interviewState, setInterviewState] = + useState("IDLE"); + const [interviewResultId, setInterviewResultId] = useState( + null + ); + + useResumeBasedInterviewEvent( + "resumeBasedInterview:submitted", + async (payload) => { + try { + setInterviewState("PENDING"); + setInterviewResultId(payload.resume_based_interview_result_id); + const response = await checkResumeBasedInterviewQuestion( + payload.resume_based_interview_result_id + ); + if (response.state === "COMPLETED") { + setInterviewState("COMPLETED"); + } else if (response.state === "FAILED") { + setInterviewState("ERROR"); + } + } catch (error) { + setInterviewState("ERROR"); + } + } + ); + + return ( + + + {interviewState === "PENDING" && ( + + + + 면접 질문 생성 중... + + + + + )} + {interviewState === "COMPLETED" && ( + { + setInterviewState("IDLE"); + setInterviewResultId(null); + }} + > + + + + 면접 질문 생성 완료 + + + + + + )} + {interviewState === "ERROR" && ( + + { + setInterviewState("IDLE"); + setInterviewResultId(null); + }} + > + + 면접 질문 생성 중
오류가 발생했어요 +
+ +
+
+ )} +
+ {children} +
+ ); +} diff --git a/apps/client/src/domains/resume/utils/resumeInterviewEventEmitter.ts b/apps/client/src/domains/resume/utils/resumeInterviewEventEmitter.ts new file mode 100644 index 00000000..4969979a --- /dev/null +++ b/apps/client/src/domains/resume/utils/resumeInterviewEventEmitter.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-unused-vars */ +import { publishEvent, useSubscribeEvents } from "@/utils/eventEmitter"; +import { + ResumeBasedInterviewEventPayloads, + ResumeBasedInterviewEventType +} from "@kokomen/types"; +import { DependencyList } from "react"; +// 이벤트에 대서 콜백 함수 구독하는 훅 +export function useResumeBasedInterviewEvent< + K extends ResumeBasedInterviewEventType +>( + event: K, + handler: ResumeBasedInterviewEventPayloads[K] extends undefined + ? () => void + : (payload: ResumeBasedInterviewEventPayloads[K]) => void, + deps: DependencyList = [] +): void { + const eventEmitter = useSubscribeEvents( + [{ event, handler }], + [] + ); +} + +export const publishResumeBasedInterviewEvent = publishEvent< + ResumeBasedInterviewEventType, + ResumeBasedInterviewEventPayloads +>(); diff --git a/apps/client/src/pages/_app.tsx b/apps/client/src/pages/_app.tsx index 196c1434..1f359b82 100644 --- a/apps/client/src/pages/_app.tsx +++ b/apps/client/src/pages/_app.tsx @@ -7,6 +7,7 @@ import { ErrorBoundary } from "@sentry/nextjs"; import ErrorFallback from "@/shared/errorFallback"; import FeedbackButton from "@/shared/feedbackButton"; import ResumeStoreProvider from "@/domains/resume/context/resumeStore"; +import ResumeBasedInterviewStoreProvider from "@/domains/resume/context/resumeBasedInterviewStore"; const queryClient: QueryClient = new QueryClient(); @@ -17,8 +18,10 @@ export default function App({ Component, pageProps }: AppProps): JSX.Element { }> - - + + + + diff --git a/apps/client/src/pages/members/interviews/[interviewId].tsx b/apps/client/src/pages/members/interviews/[interviewId].tsx index 24430614..ca198d57 100644 --- a/apps/client/src/pages/members/interviews/[interviewId].tsx +++ b/apps/client/src/pages/members/interviews/[interviewId].tsx @@ -173,7 +173,9 @@ export const getServerSideProps = async ( ]); if (interviewResult.status === "rejected") { - return { redirect: { destination: "/error", permanent: false } }; + // 크롤러가 인덱싱할 수 있도록 redirect 대신 notFound 반환 + // API가 인증 없이 접근 가능하도록 서버 측 수정이 필요할 수 있음 + return { notFound: true }; } const result = interviewResult.value; diff --git a/apps/client/src/pages/resume/index.tsx b/apps/client/src/pages/resume/index.tsx index 2b55a88a..accfa1bc 100644 --- a/apps/client/src/pages/resume/index.tsx +++ b/apps/client/src/pages/resume/index.tsx @@ -22,6 +22,7 @@ export default function ResumePage({ description="내 이력서는 채용 공고에 얼마나 적합할까? 지금 꼬꼬면에서 이력서와 포트폴리오가 채용 공고에 얼마나 적합한지 평가해보세요." image="/resume.png" robots="index, follow" + pathname="/resume" />
diff --git a/apps/client/src/pages/resume/interview/[interviewId].tsx b/apps/client/src/pages/resume/interview/[interviewId].tsx new file mode 100644 index 00000000..fc6d0540 --- /dev/null +++ b/apps/client/src/pages/resume/interview/[interviewId].tsx @@ -0,0 +1,216 @@ +import Header from "@/shared/header"; +import { getUserInfo } from "@/domains/auth/api"; +import { UserInfo } from "@kokomen/types"; +import { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType +} from "next"; +import { Footer } from "@/shared/footer"; +import { AxiosError } from "axios"; +import { SEO } from "@/shared/seo"; +import { getResumeInterviewResult } from "@/domains/resume/api/resumeBasedInterview"; +import { ResumeBasedInterviewQuestion } from "@kokomen/types"; +import { Button, useToast } from "@kokomen/ui"; +import { useRouter } from "next/router"; +import { useMutation } from "@tanstack/react-query"; +import { createResumeBasedInterview } from "@/domains/resume/api/resumeBasedInterview"; +import { InterviewMode } from "@kokomen/types"; +import { useState } from "react"; +import ResumeInterviewModeSelectModal from "@/domains/resume/components/resumeInterviewModeSelectModal"; +import { withApiErrorCapture } from "@/utils/error"; +import useExtendedRouter from "@/hooks/useExtendedRouter"; +import { MessageSquare } from "lucide-react"; + +export default function ResumeInterviewResultPage({ + userInfo, + questions, + interviewId +}: InferGetServerSidePropsType) { + const router = useRouter(); + const { toast } = useToast(); + const extendedRouter = useExtendedRouter(); + const [selectedQuestionId, setSelectedQuestionId] = useState( + null + ); + const [isModeModalOpen, setIsModeModalOpen] = useState(false); + + const createInterviewMutation = useMutation({ + mutationFn: ({ + generatedQuestionId, + mode + }: { + generatedQuestionId: number; + mode: InterviewMode; + }) => + createResumeBasedInterview({ + resumeBasedInterviewResultId: Number(interviewId), + generatedQuestionId, + maxQuestionCount: questions.length, + mode + }), + onSuccess: (data) => { + const mode = "cur_question" in data ? "TEXT" : "VOICE"; + router.push(`/interviews/${data.interview_id}?mode=${mode}`); + }, + onError: withApiErrorCapture((error) => { + if (error instanceof AxiosError && error.response?.status === 401) { + extendedRouter.navigateToLogin(); + return; + } + toast({ + title: "면접 생성 실패", + description: + "면접 생성 중 오류가 발생했어요. 잠시 후 다시 시도해주세요.", + variant: "error" + }); + }) + }); + + const handleStartInterview = (questionId: number): void => { + setSelectedQuestionId(questionId); + setIsModeModalOpen(true); + }; + + const handleSelectMode = (mode: InterviewMode): void => { + if (selectedQuestionId === null) return; + + createInterviewMutation.mutate({ + generatedQuestionId: selectedQuestionId, + mode + }); + }; + + return ( + <> + +
+
+
+
+
+
+

+ 생성된 면접 질문 +

+

+ 이력서를 기반으로 생성된 면접 질문입니다. 질문을 선택하여 + 면접을 시작해보세요. +

+
+ +
+ {questions.map((question, index) => ( +
+
+
+
+ + {index + 1} + +

+ 질문 {index + 1} +

+
+

+ {question.question} +

+
+ +
+
+ ))} +
+
+
+
+
+
+ + { + setIsModeModalOpen(false); + setSelectedQuestionId(null); + }} + onSelectMode={handleSelectMode} + /> + + ); +} + +export const getServerSideProps = async ( + context: GetServerSidePropsContext +): Promise< + GetServerSidePropsResult<{ + userInfo: UserInfo | null; + questions: ResumeBasedInterviewQuestion[]; + interviewId: string; + }> +> => { + const { interviewId } = context.params as { interviewId: string }; + const interviewIdNumber = parseInt(interviewId, 10); + + if (isNaN(interviewIdNumber)) { + return { + notFound: true + }; + } + + try { + const [userInfoResult, questionsResult] = await Promise.all([ + getUserInfo(context) + .then((res) => res.data) + .catch((error) => { + if (error instanceof AxiosError && error.response?.status === 401) { + return null; + } + throw error; + }), + getResumeInterviewResult(interviewIdNumber, context) + ]); + + return { + props: { + userInfo: userInfoResult, + questions: questionsResult as ResumeBasedInterviewQuestion[], + interviewId + } + }; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 401) { + return { + redirect: { + destination: "/login", + permanent: false + } + }; + } + if (error instanceof AxiosError) { + if (error.response?.status === 404 || error.response?.status === 403) { + return { + notFound: true + }; + } + } + throw error; + } +}; diff --git a/apps/client/src/pages/resume/interview/index.tsx b/apps/client/src/pages/resume/interview/index.tsx new file mode 100644 index 00000000..49f66ae9 --- /dev/null +++ b/apps/client/src/pages/resume/interview/index.tsx @@ -0,0 +1,59 @@ +import Header from "@/shared/header"; +import { getUserInfo } from "@/domains/auth/api"; +import { UserInfo } from "@kokomen/types"; +import { ErrorBoundary } from "@sentry/nextjs"; +import { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType +} from "next"; +import { Footer } from "@/shared/footer"; +import { AxiosError } from "axios"; +import { SEO } from "@/shared/seo"; +import ResumeBasedInterviewForm from "@/domains/resume/components/resumeBasedInterviewForm"; + +export default function ResumeInterviewCreatePage({ + userInfo +}: InferGetServerSidePropsType) { + return ( + <> + +
+
+
+
+ + + +
+
+
+
+ + ); +} + +export const getServerSideProps = async ( + context: GetServerSidePropsContext +): Promise> => { + const userInfo = await getUserInfo(context) + .then((res) => res.data) + .catch((error) => { + if (error instanceof AxiosError && error.response?.status === 401) { + return null; + } + throw error; + }); + + return { + props: { + userInfo + } + }; +}; diff --git a/apps/client/src/utils/querykeys.ts b/apps/client/src/utils/querykeys.ts index dc0151d8..2fa3e74d 100644 --- a/apps/client/src/utils/querykeys.ts +++ b/apps/client/src/utils/querykeys.ts @@ -109,6 +109,27 @@ const archiveKeys: QueryKeyFactory = { [...archiveKeys.all, "resumes", type ?? "ALL"] as const }; +type ResumeBasedInterviewMethods = { + generations: (page?: number) => QueryKey; + infinite: () => QueryKey; +}; +const resumeBasedInterviewKeys: QueryKeyFactory = { + all: ["resumeBasedInterview"] as const, + generations: (page: number = 0): QueryKey => + [...resumeBasedInterviewKeys.all, "generations", page] as const, + infinite: (): QueryKey => + [...resumeBasedInterviewKeys.all, "infinite"] as const +}; + +type ResumeEvaluationMethods = { + history: (page?: number, size?: number) => QueryKey; +}; +const resumeEvaluationKeys: QueryKeyFactory = { + all: ["resumeEvaluation"] as const, + history: (page: number = 0, size: number = 20): QueryKey => + [...resumeEvaluationKeys.all, "history", page, size] as const +}; + export { interviewHistoryKeys, interviewKeys, @@ -116,9 +137,13 @@ export { archiveKeys, purchaseKeys, recruitKeys, + resumeBasedInterviewKeys, + resumeEvaluationKeys, type InterviewHistoryParams, type InterviewParams, type MemberRankParams, type RecruitMethods, - type ArchiveMethods + type ArchiveMethods, + type ResumeBasedInterviewMethods, + type ResumeEvaluationMethods }; diff --git a/packages/types/src/events/index.ts b/packages/types/src/events/index.ts index 11786bb4..3bac9aab 100644 --- a/packages/types/src/events/index.ts +++ b/packages/types/src/events/index.ts @@ -1,2 +1,3 @@ export * from "./interview"; export * from "./report"; +export * from "./resumeBasedInterview"; diff --git a/packages/types/src/events/resumeBasedInterview.ts b/packages/types/src/events/resumeBasedInterview.ts new file mode 100644 index 00000000..f406ccfa --- /dev/null +++ b/packages/types/src/events/resumeBasedInterview.ts @@ -0,0 +1,14 @@ +interface ResumeBasedInterviewEventPayloads { + "resumeBasedInterview:submitted": { + resume_based_interview_result_id: number; + }; + "resumeBasedInterview:created": undefined; + "resumeBasedInterview:updated": undefined; + "resumeBasedInterview:error": { error: string }; +} + +type ResumeBasedInterviewEventType = keyof ResumeBasedInterviewEventPayloads; +export type { + ResumeBasedInterviewEventType, + ResumeBasedInterviewEventPayloads +}; diff --git a/packages/types/src/interviews/index.ts b/packages/types/src/interviews/index.ts index dddcc18f..7543f661 100644 --- a/packages/types/src/interviews/index.ts +++ b/packages/types/src/interviews/index.ts @@ -156,3 +156,4 @@ export type { AnswerScore, InterviewQuestion }; +export * from "./resumeBasedInterview"; diff --git a/packages/types/src/interviews/resumeBasedInterview.ts b/packages/types/src/interviews/resumeBasedInterview.ts new file mode 100644 index 00000000..fe90d490 --- /dev/null +++ b/packages/types/src/interviews/resumeBasedInterview.ts @@ -0,0 +1,52 @@ +import { InterviewMode } from "."; +import { Paginated } from "../utils"; + +type CreateResumeBasedInterview = { + generated_question_id: number; + max_question_count: number; + mode: InterviewMode; +}; + +type ResumeBasedInterviewQuestion = { + id: number; + question: string; +}; + +type ResumeInterviewPending = { + state: "PENDING"; +}; +type ResumeInterviewFailed = { + state: "FAILED"; +}; +type ResumeInterviewSuccess = { + state: "COMPLETED"; +}; + +type ResumeBasedInterviewGeneration = { + id: number; + job_career: string; + state: "PENDING" | "COMPLETED" | "FAILED"; + created_at: string; + resume: { + name: string; + url: string; + } | null; + portfolio: { + name: string; + url: string; + } | null; +}; + +type ResumeBasedInterviewGenerationsResponse = Paginated< + ResumeBasedInterviewGeneration[] +>; + +export type { + CreateResumeBasedInterview, + ResumeBasedInterviewQuestion, + ResumeInterviewPending, + ResumeInterviewFailed, + ResumeInterviewSuccess, + ResumeBasedInterviewGeneration, + ResumeBasedInterviewGenerationsResponse +}; diff --git a/packages/types/src/resume/index.ts b/packages/types/src/resume/index.ts index 8d565c1c..1e8d0af8 100644 --- a/packages/types/src/resume/index.ts +++ b/packages/types/src/resume/index.ts @@ -77,6 +77,24 @@ type ResumeEvaluationResult = { job_career: string; result: ResumeOutput["result"]; }; + +type ResumeEvaluationHistoryItem = { + id: number; + state: "PENDING" | "COMPLETED" | "FAILED"; + job_position: string; + job_career: string; + total_score: number; + created_at: string; +}; + +type ResumeEvaluationsResponse = { + evaluations: ResumeEvaluationHistoryItem[]; + current_page: number; + total_resume_evaluation_count: number; + total_pages: number; + has_next: boolean; +}; + export type { ResumeInput, ResumeOutput, @@ -85,5 +103,7 @@ export type { ResumeInputWithArchivedFile, ResumeInputWithNewFile, ResumeFailed, - ResumeEvaluationResult + ResumeEvaluationResult, + ResumeEvaluationHistoryItem, + ResumeEvaluationsResponse }; diff --git a/yarn.lock b/yarn.lock index 464e1cdf..eca99b9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5035,6 +5035,7 @@ __metadata: react-chartjs-2: "npm:^5.3.0" react-dom: "npm:^19.0.0" react-hook-form: "npm:^7.59.0" + react-pdf: "npm:^9.2.1" tailwindcss: "npm:^4" three: "npm:^0.177.0" ts-node: "npm:^10.9.2" @@ -11341,7 +11342,7 @@ __metadata: languageName: node linkType: hard -"bl@npm:^4.1.0": +"bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" dependencies: @@ -11619,6 +11620,17 @@ __metadata: languageName: node linkType: hard +"canvas@npm:^3.0.0-rc2": + version: 3.2.1 + resolution: "canvas@npm:3.2.1" + dependencies: + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.3" + checksum: 10c0/c0fd572a8b28e075b40a42b523bdf05e985feaeb18b56085432bfb91a3b905af48f89ec73ed4e795de892cb13f7332ceb0c78cf84c64281c41c29995665b89c8 + languageName: node + linkType: hard + "chai@npm:^5.1.1": version: 5.2.0 resolution: "chai@npm:5.2.0" @@ -11741,6 +11753,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -11950,7 +11969,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.1.1": +"clsx@npm:^2.0.0, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839 @@ -12503,6 +12522,15 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e + languageName: node + linkType: hard + "dedent@npm:^1.6.0": version: 1.6.0 resolution: "dedent@npm:1.6.0" @@ -12522,6 +12550,13 @@ __metadata: languageName: node linkType: hard +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -12629,6 +12664,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "detect-libc@npm:^2.0.3, detect-libc@npm:^2.0.4": version: 2.0.4 resolution: "detect-libc@npm:2.0.4" @@ -12851,7 +12893,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.5 resolution: "end-of-stream@npm:1.4.5" dependencies: @@ -13806,6 +13848,13 @@ __metadata: languageName: node linkType: hard +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51 + languageName: node + linkType: hard + "expect-type@npm:^1.2.1": version: 1.2.2 resolution: "expect-type@npm:1.2.2" @@ -14390,6 +14439,13 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + "fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -14576,6 +14632,13 @@ __metadata: languageName: node linkType: hard +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -15124,6 +15187,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a + languageName: node + linkType: hard + "inquirer@npm:^6.3.1": version: 6.5.2 resolution: "inquirer@npm:6.5.2" @@ -17797,7 +17867,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -17933,6 +18003,13 @@ __metadata: languageName: node linkType: hard +"make-cancellable-promise@npm:^1.3.1": + version: 1.3.2 + resolution: "make-cancellable-promise@npm:1.3.2" + checksum: 10c0/10aa0450c743dcf20b55414c433ca45926b775b22eb6d25fa386fc499a8f3fc64c70eb575d99bdd16667d300068f51702822c293bc4e72da7ff4f82d0ea48184 + languageName: node + linkType: hard + "make-dir@npm:^3.0.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -17958,6 +18035,13 @@ __metadata: languageName: node linkType: hard +"make-event-props@npm:^1.6.0": + version: 1.6.2 + resolution: "make-event-props@npm:1.6.2" + checksum: 10c0/ecf0b742e43a392c07e2267baca2397e750d38cc14ef3cb72ef8bfe4a8c8b0fd99a03a2eeab84a26c2b204f7c231da6af31fa26321fbfd413ded43ba1825e867 + languageName: node + linkType: hard + "make-fetch-happen@npm:^14.0.3": version: 14.0.3 resolution: "make-fetch-happen@npm:14.0.3" @@ -18060,6 +18144,18 @@ __metadata: languageName: node linkType: hard +"merge-refs@npm:^1.3.0": + version: 1.3.0 + resolution: "merge-refs@npm:1.3.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/403d20d283a595565a6bef813415df509dad12a5ad157f0ae04861b3aee4a3691971ccae7079e20497d9f367a478ad60e5b63a2ca9ffb2cc3d511284b49b4bd6 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -18178,6 +18274,13 @@ __metadata: languageName: node linkType: hard +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -18221,7 +18324,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -18318,6 +18421,13 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + "mkdirp@npm:^0.5.3, mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -18500,6 +18610,13 @@ __metadata: languageName: node linkType: hard +"napi-build-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "napi-build-utils@npm:2.0.0" + checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db + languageName: node + linkType: hard + "napi-postinstall@npm:^0.2.4": version: 0.2.5 resolution: "napi-postinstall@npm:0.2.5" @@ -18629,6 +18746,15 @@ __metadata: languageName: node linkType: hard +"node-abi@npm:^3.3.0": + version: 3.87.0 + resolution: "node-abi@npm:3.87.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/41cfc361edd1b0711d412ca9e1a475180c5b897868bd5583df7ff73e30e6044cc7de307df36c2257203320f17fadf7e82dfdf5a9f6fd510a8578e3fe3ed67ebb + languageName: node + linkType: hard + "node-abort-controller@npm:^3.0.1, node-abort-controller@npm:^3.1.1": version: 3.1.1 resolution: "node-abort-controller@npm:3.1.1" @@ -18636,6 +18762,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9 + languageName: node + linkType: hard + "node-emoji@npm:1.11.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" @@ -19167,6 +19302,13 @@ __metadata: languageName: node linkType: hard +"path2d@npm:^0.2.1": + version: 0.2.2 + resolution: "path2d@npm:0.2.2" + checksum: 10c0/1bb76c7f275d07f1bc7ca12171d828e91bf8a12596f0765a52e9d4d47fe1a428455dc1dd4c9002924a9bc554f6ac25e09a6c22eaecf32e5e33fba2985b5168f8 + languageName: node + linkType: hard + "path@npm:^0.12.7": version: 0.12.7 resolution: "path@npm:0.12.7" @@ -19191,6 +19333,21 @@ __metadata: languageName: node linkType: hard +"pdfjs-dist@npm:4.8.69": + version: 4.8.69 + resolution: "pdfjs-dist@npm:4.8.69" + dependencies: + canvas: "npm:^3.0.0-rc2" + path2d: "npm:^0.2.1" + dependenciesMeta: + canvas: + optional: true + path2d: + optional: true + checksum: 10c0/dc297f2a36aa36834a2892cb78c3cafc7ac01753a2e7c4316a1f6e8c1d337a52a3bfbf7fdff7aaba615893b53f2d06a0efc2176525592b4d7b51021279c101be + languageName: node + linkType: hard + "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -19399,6 +19556,28 @@ __metadata: languageName: node linkType: hard +"prebuild-install@npm:^7.1.3": + version: 7.1.3 + resolution: "prebuild-install@npm:7.1.3" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^2.0.0" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -19668,6 +19847,20 @@ __metadata: languageName: node linkType: hard +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15 + languageName: node + linkType: hard + "react-chartjs-2@npm:^5.3.0": version: 5.3.0 resolution: "react-chartjs-2@npm:5.3.0" @@ -19753,6 +19946,29 @@ __metadata: languageName: node linkType: hard +"react-pdf@npm:^9.2.1": + version: 9.2.1 + resolution: "react-pdf@npm:9.2.1" + dependencies: + clsx: "npm:^2.0.0" + dequal: "npm:^2.0.3" + make-cancellable-promise: "npm:^1.3.1" + make-event-props: "npm:^1.6.0" + merge-refs: "npm:^1.3.0" + pdfjs-dist: "npm:4.8.69" + tiny-invariant: "npm:^1.0.0" + warning: "npm:^4.0.0" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/69b5456b3941ea08f03319a94b155db782232dee4b3e03513c4a4c10cc3d81d129fc3284136990b51d5dcf766192abc64d71e1d258ca7e0eb4e6592343fea6a4 + languageName: node + linkType: hard + "react-reconciler@npm:^0.31.0": version: 0.31.0 resolution: "react-reconciler@npm:0.31.0" @@ -19803,7 +20019,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.0.2, readable-stream@npm:^3.4.0": +"readable-stream@npm:^3.0.2, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -20911,6 +21127,24 @@ __metadata: languageName: node linkType: hard +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776 + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0 + languageName: node + linkType: hard + "simple-swizzle@npm:^0.2.2": version: 0.2.2 resolution: "simple-swizzle@npm:0.2.2" @@ -21415,6 +21649,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43 + languageName: node + linkType: hard + "strip-literal@npm:^3.0.0": version: 3.0.0 resolution: "strip-literal@npm:3.0.0" @@ -21608,6 +21849,18 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^2.0.0": + version: 2.1.4 + resolution: "tar-fs@npm:2.1.4" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c + languageName: node + linkType: hard + "tar-fs@npm:^3.0.8": version: 3.1.0 resolution: "tar-fs@npm:3.1.0" @@ -21625,6 +21878,19 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + "tar-stream@npm:^3.1.5": version: 3.1.7 resolution: "tar-stream@npm:3.1.7" @@ -21773,7 +22039,7 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:*, tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3": +"tiny-invariant@npm:*, tiny-invariant@npm:^1.0.0, tiny-invariant@npm:^1.3.1, tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" checksum: 10c0/65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a @@ -22208,6 +22474,15 @@ __metadata: languageName: node linkType: hard +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a + languageName: node + linkType: hard + "tunnel-rat@npm:^0.1.2": version: 0.1.2 resolution: "tunnel-rat@npm:0.1.2" @@ -23144,6 +23419,15 @@ __metadata: languageName: node linkType: hard +"warning@npm:^4.0.0": + version: 4.0.3 + resolution: "warning@npm:4.0.3" + dependencies: + loose-envify: "npm:^1.0.0" + checksum: 10c0/aebab445129f3e104c271f1637fa38e55eb25f968593e3825bd2f7a12bd58dc3738bb70dc8ec85826621d80b4acfed5a29ebc9da17397c6125864d72301b937e + languageName: node + linkType: hard + "watchpack@npm:^2.4.1": version: 2.4.4 resolution: "watchpack@npm:2.4.4"