diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..e10a46d --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +module.exports = { + semi: false, + singleQuote: true, + trailingComma: 'all', +} diff --git a/package-lock.json b/package-lock.json index d204898..d472808 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,22 +12,31 @@ "@ckeditor/ckeditor5-react": "^6.1.0", "@types/bcrypt": "^5.0.1", "@types/date-fns": "^2.6.0", + "@types/formidable": "^3.4.5", + "@types/multer": "^1.4.10", "@types/react-datepicker": "^4.19.3", "axios": "^1.6.1", "bcrypt": "^5.1.1", "date-fns": "^2.30.0", "dotenv": "^16.3.1", + "formidable": "^3.5.1", "gsap": "^3.12.2", "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", + "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql2": "^3.6.3", "next": "14.0.1", "next-auth": "^4.24.4", + "next-connect": "^1.0.0", "next-themes": "^0.2.1", + "prettier": "^3.1.0", "react": "^18", + "react-calendar": "^4.6.1", "react-datepicker": "^4.21.0", - "react-dom": "^18" + "react-dom": "^18", + "react-modal": "^3.16.1", + "recoil": "^0.7.7" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.4", @@ -798,6 +807,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/bcrypt": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.1.tgz", @@ -806,6 +820,23 @@ "@types/node": "*" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/date-fns": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.6.0.tgz", @@ -815,6 +846,41 @@ "date-fns": "*" } }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/formidable": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-3.4.5.tgz", + "integrity": "sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -843,6 +909,19 @@ "@types/lodash": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/multer": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.10.tgz", + "integrity": "sha512-6l9mYMhUe8wbnz/67YIjc7ZJyQNZoKq7fRXVf7nMdgWgalD0KyzJ2ywI7hoATUSXSbTu9q2HBiEwzy0tNN1v2w==", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.8.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", @@ -856,6 +935,16 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, + "node_modules/@types/qs": { + "version": "6.9.10", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + }, + "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==" + }, "node_modules/@types/react": { "version": "18.2.35", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.35.tgz", @@ -900,6 +989,25 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz", "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", @@ -1111,6 +1219,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -1292,6 +1405,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -1482,6 +1600,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1607,6 +1730,26 @@ "node": ">=10" } }, + "node_modules/ckeditor5": { + "version": "40.0.0", + "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-40.0.0.tgz", + "integrity": "sha512-pyXptFXODCyICkIaiBfWl+vqNq87CwB4IodaacUDdp+7z7szK13/vMJMWyPCQyZob+lzpakqPpj29HR5Kzy4AQ==", + "dependencies": { + "@ckeditor/ckeditor5-clipboard": "40.0.0", + "@ckeditor/ckeditor5-core": "40.0.0", + "@ckeditor/ckeditor5-engine": "40.0.0", + "@ckeditor/ckeditor5-enter": "40.0.0", + "@ckeditor/ckeditor5-paragraph": "40.0.0", + "@ckeditor/ckeditor5-select-all": "40.0.0", + "@ckeditor/ckeditor5-typing": "40.0.0", + "@ckeditor/ckeditor5-ui": "40.0.0", + "@ckeditor/ckeditor5-undo": "40.0.0", + "@ckeditor/ckeditor5-upload": "40.0.0", + "@ckeditor/ckeditor5-utils": "40.0.0", + "@ckeditor/ckeditor5-watchdog": "40.0.0", + "@ckeditor/ckeditor5-widget": "40.0.0" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -1682,6 +1825,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -1843,6 +2000,15 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2630,6 +2796,19 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", + "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2916,6 +3095,11 @@ "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz", "integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ==" }, + "node_modules/hamt_plus": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", + "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3002,6 +3186,14 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3740,6 +3932,14 @@ "semver": "bin/semver.js" } }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3796,7 +3996,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3856,6 +4055,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -4029,6 +4256,18 @@ } } }, + "node_modules/next-connect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz", + "integrity": "sha512-FeLURm9MdvzY1SDUGE74tk66mukSqL6MAzxajW7Gqh6DZKBZLrXmXnGWtHJZXkfvoi+V/DUe9Hhtfkl4+nTlYA==", + "dependencies": { + "@tsconfig/node16": "^1.0.3", + "regexparam": "^2.0.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/next-themes": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", @@ -4575,6 +4814,20 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", @@ -4640,6 +4893,31 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.1.tgz", + "integrity": "sha512-MvCPdvxEvq7wICBhFxlYwxS2+IsVvSjTcmlr0Kl3yDRVhoX7btNg0ySJx5hy9rb1eaM4nDpzQcW5c87nfQ8n8w==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "prop-types": "^15.6.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-datepicker": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.21.0.tgz", @@ -4679,6 +4957,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/react-onclickoutside": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", @@ -4746,6 +5047,25 @@ "node": ">=8.10.0" } }, + "node_modules/recoil": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", + "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", + "dependencies": { + "hamt_plus": "1.0.2" + }, + "peerDependencies": { + "react": ">=16.13.1" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -4783,6 +5103,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.1.tgz", + "integrity": "sha512-zRgSaYemnNYxUv+/5SeoHI0eJIgTL/A2pUtXUPLHQxUldagouJ9p+K6IbIZ/JiQuCEv2E2B1O11SjVQy3aMCkw==", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5430,6 +5758,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -5495,6 +5835,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -5580,6 +5925,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vanilla-colorful": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz", + "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==" + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -5718,6 +6068,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 304b599..1a05080 100644 --- a/package.json +++ b/package.json @@ -13,24 +13,31 @@ "@ckeditor/ckeditor5-react": "^6.1.0", "@types/bcrypt": "^5.0.1", "@types/date-fns": "^2.6.0", + "@types/formidable": "^3.4.5", + "@types/multer": "^1.4.10", "@types/react-datepicker": "^4.19.3", "axios": "^1.6.1", "bcrypt": "^5.1.1", "date-fns": "^2.30.0", "dotenv": "^16.3.1", + "formidable": "^3.5.1", "gsap": "^3.12.2", "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", + "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql2": "^3.6.3", "next": "14.0.1", "next-auth": "^4.24.4", + "next-connect": "^1.0.0", "next-themes": "^0.2.1", + "prettier": "^3.1.0", "react": "^18", "react-calendar": "^4.6.1", "react-datepicker": "^4.21.0", "react-dom": "^18", - "react-modal": "^3.16.1" + "react-modal": "^3.16.1", + "recoil": "^0.7.7" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.4", diff --git a/public/imgs/1699932163197_WoIlN4fvGVzXA9SWory87ePQHb9OTq81 b/public/imgs/1699932163197_WoIlN4fvGVzXA9SWory87ePQHb9OTq81 new file mode 100644 index 0000000..0d05543 Binary files /dev/null and b/public/imgs/1699932163197_WoIlN4fvGVzXA9SWory87ePQHb9OTq81 differ diff --git a/public/imgs/1699932201648_M80SJmXFU98hSz19ftU5ZA7Llw5UnWtk b/public/imgs/1699932201648_M80SJmXFU98hSz19ftU5ZA7Llw5UnWtk new file mode 100644 index 0000000..8043329 Binary files /dev/null and b/public/imgs/1699932201648_M80SJmXFU98hSz19ftU5ZA7Llw5UnWtk differ diff --git a/public/imgs/1699932246835_2E9u0YKK0qnEjL4Qb5aTxiJUAhOmt7Wz b/public/imgs/1699932246835_2E9u0YKK0qnEjL4Qb5aTxiJUAhOmt7Wz new file mode 100644 index 0000000..119a428 Binary files /dev/null and b/public/imgs/1699932246835_2E9u0YKK0qnEjL4Qb5aTxiJUAhOmt7Wz differ diff --git a/public/imgs/1699932284800_yi862zgmllcb4h7KzeLzGq3LFaGkwIxP.png b/public/imgs/1699932284800_yi862zgmllcb4h7KzeLzGq3LFaGkwIxP.png new file mode 100644 index 0000000..25026ac Binary files /dev/null and b/public/imgs/1699932284800_yi862zgmllcb4h7KzeLzGq3LFaGkwIxP.png differ diff --git a/public/imgs/1699932377524_q28ucKw2qv7zceIO.png b/public/imgs/1699932377524_q28ucKw2qv7zceIO.png new file mode 100644 index 0000000..0d05543 Binary files /dev/null and b/public/imgs/1699932377524_q28ucKw2qv7zceIO.png differ diff --git a/public/imgs/1699932690871_6ziy3nprERU56YTy.png b/public/imgs/1699932690871_6ziy3nprERU56YTy.png new file mode 100644 index 0000000..0d05543 Binary files /dev/null and b/public/imgs/1699932690871_6ziy3nprERU56YTy.png differ diff --git "a/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-11 100559.png" "b/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-11 100559.png" new file mode 100644 index 0000000..0d05543 Binary files /dev/null and "b/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-11 100559.png" differ diff --git "a/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-15 085650.png" "b/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-15 085650.png" new file mode 100644 index 0000000..25026ac Binary files /dev/null and "b/public/imgs/\354\212\244\355\201\254\353\246\260\354\203\267 2023-09-15 085650.png" differ diff --git a/src/app/api/advice/route.ts b/src/app/api/advice/route.ts new file mode 100644 index 0000000..41a79c1 --- /dev/null +++ b/src/app/api/advice/route.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; +import { NextResponse } from 'next/server'; +import { verifyJwt } from '@/app/lib/jwt'; + +interface reqBody { + text: string; +} +type resBody = { + result: string; +} + +export const POST = async(req: Request) => { + const body: reqBody = await req.json(); + const accessToken = req.headers.get('Authorization')?.split('mlru ')[1] as string; + + // 헤더에 토큰이 없거나, 토큰 복호화 실패하면 리턴. + if(!accessToken || !verifyJwt(accessToken)) { + return new Response(JSON.stringify({"result":"No Authorization"})) + } + try { + const res = await axios.post(`${process.env.MODEL_URL}/advice/`,{ + text: body.text + }); + const result = res.data + return NextResponse.json(result.result) + } catch (err) { + console.log(err) + return NextResponse.json(err) + } + +}; \ No newline at end of file diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 0b98231..0d02cf8 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -29,7 +29,7 @@ const handler = NextAuth({ if(result.user_id) { return result; } - throw new Error(result); + throw new Error(result.result); } }), KakaoProvider({ @@ -53,6 +53,7 @@ const handler = NextAuth({ account.refresh_token = result.refreshToken return true; } else { + // 로컬 로그인 처리 쪽인데 어떻게 할까. return true; } }, diff --git a/src/app/api/diary/route.ts b/src/app/api/diary/route.ts new file mode 100644 index 0000000..2f96f7c --- /dev/null +++ b/src/app/api/diary/route.ts @@ -0,0 +1,174 @@ +import { NextRequest, NextResponse } from "next/server"; +import axios from 'axios'; +import queryPromise from "@/app/lib/db"; +export const api = { + bodyParse: false +}; + +// POST 작성 +// PATCH 수정 +// DELETE 삭제 + +export async function POST( req: NextRequest, res: NextResponse ) { + const data = await req.formData(); + const accessToken = req.headers.get('Authorization')?.split('mlru ')[1] as string; + const title = data.get('title') as string; + const content = data.get('content') as string; + const weather = data.get('weather') as string; + const id = data.get('id') as string; + const name = data.get('name') as string; + const imgTit = data.get('imgTit') as string; + + + const predictEmo = await axios.post( + `${process.env.BASE_URL}/api/emotion`, + {text: content}, + { + headers: { + 'Authorization': `mlru ${accessToken}` + } + } + ); + console.log(predictEmo.data); // 감정 숫자. + + const maxEmotion = Object.entries(predictEmo.data).reduce((max: any, [key, value]: any) => { + return value > max[1] ? [key, value] : max; + }, ['', -Infinity]); + + const predictSumm = await axios.post( + `${process.env.BASE_URL}/api/summary`, + {text: content}, + { + headers: { + 'Authorization': `mlru ${accessToken}` + } + } + ); + + console.log(predictSumm.data); // 내용 요약. + + const predictAdvice = await axios.post( + `${process.env.BASE_URL}/api/advice`, + {text:predictSumm.data}, + { + headers: { + 'Authorization': `mlru ${accessToken}` + } + } + ); + console.log(predictAdvice.data); // 조언 + + const weatherQuery: { [key: string]: string } = { + "맑음": "sunny", + "흐림":"cloudy", + "비":"rainy", + "바람":"windy", + "눈":"snowy" + }; + const emotionQuery: { [key: string]: string } = { + "중립": "normal", + "슬픔": "sadness", + "분노": "angry", + "놀람": "amazing", + "행복": "happiness", + "불안": "unhappiness" + }; + const query = 'weather is ' + weatherQuery[weather] + `, feel ${emotionQuery[maxEmotion[0]]} in the picture`; + let imgSrc = []; + const predictImg = await axios.post( + `${process.env.BASE_URL}/api/img`, + {text: query}, + { + headers: { + 'Authorization': `mlru ${accessToken}` + } + } + ); + imgSrc.push(predictImg.data.result); + const img = data.get('img') as File; + if(img) { + const fb = new FormData(); + fb.append('image', img); + const result = await axios.post( + 'https://api.imgur.com/3/upload', + fb, + { + headers: { + 'Authorization': `Client-ID ${process.env.IMGUR_KEY}`, + 'Accept': 'application/json' + } + } + ); + imgSrc.push(result.data.data.link); + } + + try { + let sql = 'INSERT INTO tb_diary VALUES(?,?,?,?,?,?,?,?,?,?,?,?)'; + let values = [ + null, + id, + name, + title, + content, + predictEmo.data, + weather, + 'pretendard', + null, + predictAdvice.data, + new Date(), + new Date() + ]; + const result = await queryPromise(sql, values); + if(img) { + sql = 'INSERT INTO tb_image VALUES(?,?,?,?,?,?)'; + values = [null, result.insertId, imgTit, imgSrc, 'user', new Date()]; + const done = await queryPromise(sql, values); + } + return NextResponse.json({result:'done'}); + } catch (err) { + console.log(err); + return NextResponse.json({result:'error'}) + } +}; + +export const PUT = async(req: Request) => { + const data = await req.formData(); + const images: any[] = []; + data.forEach((v, k) => { + images.push(v); + }) + const imgs: any[] = []; + +const uploadImages = async () => { + for (const v of images) { + const fb = new FormData(); + fb.append('image', v); + + try { + const result = await axios.post( + 'https://api.imgur.com/3/upload', + fb, + { + headers: { + 'Authorization': `Client-ID ${process.env.IMGUR_KEY}`, + 'Accept': 'application/json' + } + } + ); + imgs.push(result.data.data.link); + } catch (error) { + console.log(error); + return 0; + } + } + return imgs; +}; + +const result = await uploadImages(); +if(result === 0) { + return NextResponse.json({result:'error'}); +} +console.log(result); + + return NextResponse.json({result:imgs}) +} \ No newline at end of file diff --git a/src/app/api/emotion/route.ts b/src/app/api/emotion/route.ts index d61bc87..b766f5a 100644 --- a/src/app/api/emotion/route.ts +++ b/src/app/api/emotion/route.ts @@ -4,7 +4,7 @@ import { verifyJwt } from '@/app/lib/jwt'; type reqBody = { - text: string; + text: string[]; } type resBody = { result: string; @@ -19,7 +19,7 @@ export async function POST (req: Request) { return new Response(JSON.stringify({"result":"No Authorization"})) } try { - const res = await axios.post(`http://127.0.0.1:8000/predict/emotion/`,{ + const res = await axios.post(`${process.env.MODEL_URL}/emotion/`,{ text: body.text }); const result: resBody = res.data diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 3ff0987..6ed8177 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -1,5 +1,6 @@ import { signJwtAccessToken, signJwtRefreshToken } from '@/app/lib/jwt' import queryPromise from '@/app/lib/db' +import { NextResponse } from 'next/server'; const crypto = require('crypto'); interface reqBody { @@ -10,9 +11,8 @@ interface reqBody { export const POST = async(req: Request) => { const body: reqBody = await req.json(); let sql = 'SELECT user_id, user_password, user_name, user_salt FROM tb_user WHERE user_id = ?'; - let values = [body.username]; - let result = await queryPromise(sql, values); - if(result.length < 1) return new Response(JSON.stringify({"result":"no user"})); + let result = await queryPromise(sql, [body.username]); + if(result.length < 1) return NextResponse.json({result:'아이디가 없습니다.'}) const hashPassword = crypto.createHash('sha512').update(body.password + result[0].user_salt).digest('hex'); const chk = hashPassword === result[0].user_password; if(chk) { @@ -28,5 +28,5 @@ export const POST = async(req: Request) => { refreshToken }; return new Response(JSON.stringify(rst)) - } else return new Response("Wrong Password"); + } else return NextResponse.json({result:'비밀번호가 일치하지 않습니다.'}) } \ No newline at end of file diff --git a/src/app/api/summary/route.ts b/src/app/api/summary/route.ts index 239e1c1..10da849 100644 --- a/src/app/api/summary/route.ts +++ b/src/app/api/summary/route.ts @@ -18,7 +18,7 @@ export async function POST (req: Request) { return new Response(JSON.stringify({"result":"No Authorization"})) } try { - const res = await axios.post(`http://127.0.0.1:8000/predict/summary/`,{ + const res = await axios.post(`${process.env.MODEL_URL}/summary/`,{ text: body.text }); const result: resBody = res.data diff --git a/src/app/api/test/route.ts b/src/app/api/test/route.ts index 4897a8d..6747c01 100644 --- a/src/app/api/test/route.ts +++ b/src/app/api/test/route.ts @@ -24,10 +24,10 @@ export async function POST(req: Request) { `; // 쿼리 실행 - await queryPromise(sql, [user_id, emotion_img, user_pw, user_name, provider]); - + const result = await queryPromise(sql, [user_id, emotion_img, user_pw, user_name, provider]); + console.log('test------------------------') // 성공적인 응답 반환 - const result = { emotion_img }; + console.log(result); // NextResponse 생성자를 사용하여 응답 반환 return new NextResponse(JSON.stringify(result), { diff --git a/src/app/api/trans/route.ts b/src/app/api/trans/route.ts index 1f82163..f9799c9 100644 --- a/src/app/api/trans/route.ts +++ b/src/app/api/trans/route.ts @@ -18,7 +18,7 @@ export async function POST (req: Request) { return new Response(JSON.stringify({"result":"No Authorization"})) } try { - const res = await axios.post(`http://127.0.0.1:8000/predict/trans/`,{ + const res = await axios.post(`${process.env.MODEL_URL}/trans/`,{ text: body.text }); const result: resBody = res.data diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index bb59558..9a90f49 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,39 +1,123 @@ import queryPromise from '@/app/lib/db' -import bcrypt from 'bcrypt' -import { randomStrings } from '@/app/hooks/hooks'; +import { NextResponse } from 'next/server'; +import axios from 'axios'; const crypto = require('crypto'); interface reqBody { - user_id: string; - password: string; - user_name: string; + user_id?: string; + password?: string; + user_name?: string; } +export const api = { + bodyParse: false +}; +// POST는 아이디 있는지 검증 ㅎㅅㅎ export const POST = async(req: Request) => { const body: reqBody = await req.json(); - // 쿼리 가져옴. try { - let sql = 'SELECT user_id from tb_user WHERE user_id = ?' + let sql = 'SELECT user_id from tb_user WHERE user_id = ?'; const chk = await queryPromise(sql, [body.user_id]); if(chk.length >= 1) { - // 아이디가 이미 있다. - return new Response(JSON.stringify({"result":"exists"})) + // 아이디 존재. + return NextResponse.json({result:'이미 있는 아이디예요.'}); } - sql = 'INSERT INTO tb_user VALUES(?,?,?,?,?,?,?,?,?)' + return NextResponse.json({result:'가입할 수 있는 아이디예요.'}) + } catch(err: any) { + console.log(err); + return NextResponse.json({result:'에러에러에러.'}) + } +}; + +// PUT을 회원 가입 메소드로 ㅎㅇ +// 프로필 이미지 저장 추가해야 됨. +export const PUT = async(req: Request) => { + // 이미지 추가가 있으므로 + // formData 써야함. + const data = await req.formData(); + const id = data.get('user_id') as string; + const password = data.get('password') as string; + const name = data.get('user_name') as string; + const img = data.get('img') as File; + let imgSrc = ''; + if(img) { + const fb = new FormData(); + fb.append('image', img); + const result = await axios.post('https://api.imgur.com/3/upload', + fb, + { + headers: { + 'Authorization': `Client-ID ${process.env.IMGUR_KEY}`, + 'Accept':'application/json' + } + } + ); + imgSrc = result.data.data.link; + } + try { + let sql = 'INSERT INTO tb_user VALUES(?,?,?,?,?,?,?,?,?)'; const salt = crypto.randomBytes(64).toString('base64'); // salt 생성. - const hashPassword = crypto.createHash('sha512').update(body.password + salt).digest('hex'); - // 비밀번호 뒤에 salt를 붙여서 암호화. + const hashPassword = crypto.createHash('sha512').update(password + salt).digest('hex'); + // 암호화. - const values = [null, body.user_id, hashPassword, salt, body.user_name, 'credentials', new Date(), new Date(), 'no image'] + const values = [ + null, id, hashPassword, salt, name, 'credentials', new Date(), new Date(), imgSrc + ]; const rst = await queryPromise(sql, values); return new Response(JSON.stringify({"result":"done"})); - + } catch(err: any) { console.log(err) - return new Response(JSON.stringify('error')) + return new Response(JSON.stringify('error')); } -} \ No newline at end of file +}; + +// PATCH는 정보 수정. ㅇㅅㅇ;; +export const PATCH = async(req: Request) => { + // 이미지를 수정할 수도 있으니 + // multipart/data + // formData로 받아와야 될 듯. + return NextResponse.json({result:'modify'}); +}; + +// DELETE는 회원 탈퇴. ㅂㅂ +export const DELETE = async(req: Request) => { + const body: reqBody = await req.json(); + + // 아이디는 이미 로그인 한 상태니까 + // 비밀번호만 검증 후 + // 맞으면 탈퇴, 아니면 리턴. + try { + let sql = 'SELECT user_password, user_salt from tb_user WHERE user_id = ?'; + const chk = await queryPromise(sql, [body.user_id]); + if(chk.length < 1) { + // 아이디에 맞는 비밀번호를 못 찾음. + return NextResponse.json({result:'뭔가가 이상함.'}); + } + const salt = chk[0].user_salt; + const db_pw = chk[0].user_password; + const hashPassword = crypto.createHash('sha512').update(body.password + salt).digest('hex'); + const result = db_pw === hashPassword; + if(result) { + // 비밀번호 일치. + sql = 'DELETE FROM tb_user WHERE user_id = ?'; + await queryPromise(sql, [body.user_id]); + return NextResponse.json({result:'다음에 또 볼 수 있겠죠.'}); + } else { + return NextResponse.json({result:'비밀번호가 맞지 않아요.'}) + } + } catch(err: any) { + console.log(err); + return NextResponse.json({result:'에러에러에러.'}) + } +}; + +export const OPTIONS = async(req: Request) => { + const data = await req.formData(); + console.log(data.get('user_id')) + return NextResponse.json({result:'테스트 용도'}) +}; \ No newline at end of file diff --git a/src/app/api/user/token/route.ts b/src/app/api/user/token/route.ts index 07a72b8..9d8d037 100644 --- a/src/app/api/user/token/route.ts +++ b/src/app/api/user/token/route.ts @@ -2,19 +2,20 @@ import { signJwtAccessToken, signJwtRefreshToken, verifyRefresh, verifyJwt } fro import queryPromise from "@/app/lib/db"; export const POST = async(req: Request) => { + console.log('fetched...') // 헤더에서 mlru로 된 accessToken 값 가져오기. const accessToken = req.headers.get('Authorization')?.split('mlru ')[1] as string; // 헤더에서 refreshToken 값 가져오기. let refreshToken = req.headers.get('refreshToken') as string; - // accessToken을 복호화. - const decoded = verifyJwt(accessToken); + const decoded = verifyJwt(accessToken); // 현재 시간. - const currentTime = Math.floor(Date.now() / 1000); + const currentTime = Math.floor(Date.now() / 1000) - 60000; // 1분 전에 갱신하기. // 복호화 된 값이 없다면 => 인증 실패. + if(decoded === 'signout') return new Response(JSON.stringify({"result":"signout"})); if(!decoded) return new Response(JSON.stringify({"result":"No Authorization", "status":"error"})) // 만료 기간이 지나지 않았다면 => 리턴. diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx index 6035470..be7c586 100644 --- a/src/app/calendar/page.tsx +++ b/src/app/calendar/page.tsx @@ -6,22 +6,26 @@ import 'react-calendar/dist/Calendar.css'; import moment, { MomentInput } from 'moment'; import Modal from 'react-modal'; import axios from 'axios'; +import { useSession } from 'next-auth/react'; function MyApp() { const [value, onChange] = useState(new Date()); const [modalIsOpen, setModalIsOpen] = useState(false); const [emotionSticker, setEmotionSticker] = useState(''); + const { data: session } = useSession(); const imageSrc = './KakaoTalk_20231109_164231435.png'; useEffect(() => { const fetchUserDataFromDB = async () => { try { - const dateValue = Array.isArray(value) ? (value as Date[])[0] : (value as Date); - const response = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/api/test/`, + // const dateValue = Array.isArray(value) ? (value as Date[])[0] : (value as Date); + const response = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/api/test`, { - data: moment(dateValue).format('YYYY-MM-DDTHH:mm:ss') + data: { + + } }); const data = await response.data; diff --git a/src/app/components/Providers.tsx b/src/app/components/Providers.tsx index e0d0494..a0e0f51 100644 --- a/src/app/components/Providers.tsx +++ b/src/app/components/Providers.tsx @@ -2,13 +2,22 @@ import { SessionProvider } from "next-auth/react"; import React, { ReactNode } from 'react'; +import { + RecoilRoot, +} from 'recoil' interface Props { children: ReactNode } function Providers({children}: Props) { - return {children} + return ( + + + {children} + + + ) }; export default Providers; \ No newline at end of file diff --git a/src/app/components/RefreshToken.tsx b/src/app/components/RefreshToken.tsx index 7cd54be..63f7104 100644 --- a/src/app/components/RefreshToken.tsx +++ b/src/app/components/RefreshToken.tsx @@ -1,7 +1,7 @@ "use client"; import axios from 'axios'; -import { useSession } from 'next-auth/react'; +import { useSession, signOut } from 'next-auth/react'; import { useEffect } from 'react'; const RefreshToken = () => { const { data: session } = useSession(); @@ -22,10 +22,18 @@ const RefreshToken = () => { console.log(result.result); } else if(result.status === 'ok') { if (session) session.accessToken = result.result.token; + } else if(result.result === 'signout') { + alert('다시 로그인해 주세요.'); + signOut(); } } useEffect(()=>{ - refreshAccess(); + const checking = setInterval(() => { + refreshAccess(); + }, 60 * 30 * 1000); + return () => { + clearInterval(checking); + } },[session]) return ( <> diff --git a/src/app/diary/_components/DiaryLayout.tsx b/src/app/diary/_components/DiaryLayout.tsx index 6748df5..32713ec 100644 --- a/src/app/diary/_components/DiaryLayout.tsx +++ b/src/app/diary/_components/DiaryLayout.tsx @@ -5,7 +5,7 @@ const Diary = () => {
-
+
diff --git a/src/app/diary/page.tsx b/src/app/diary/page.tsx index 838a5bf..a432575 100644 --- a/src/app/diary/page.tsx +++ b/src/app/diary/page.tsx @@ -6,11 +6,15 @@ import DatePicker from 'react-datepicker' import 'react-datepicker/dist/react-datepicker.css'; import { ko } from 'date-fns/esm/locale' import DiaryLayout from './_components/DiaryLayout'; +import { useRecoilState } from 'recoil' +import { textState } from '@/app/lib/atoms/atom' const Diary = () => { const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); + const [test, setText] = useRecoilState(textState); + console.log(test); const CalendarInput = forwardRef(({ value, onClick }: any, ref: any) => ( // any 안 쓰고 싶은데 몰루겠다... @@ -59,7 +63,7 @@ const Diary = () => { {'이전'}
- 1 + 1 2 3 4 diff --git a/src/app/hooks/hooks.ts b/src/app/hooks/hooks.ts index 3fdc1c9..e47932d 100644 --- a/src/app/hooks/hooks.ts +++ b/src/app/hooks/hooks.ts @@ -2,7 +2,7 @@ export const randomStrings = () => { const char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; - for(let i = 0; i < 32; i++) { + for(let i = 0; i < 16; i++) { const randomIndex = Math.floor(Math.random() * char.length); result += char.charAt(randomIndex); } diff --git a/src/app/icontest/contenttest/page.tsx b/src/app/icontest/contenttest/page.tsx new file mode 100644 index 0000000..4e9154e --- /dev/null +++ b/src/app/icontest/contenttest/page.tsx @@ -0,0 +1,116 @@ +'use client' + +import React, { useState, useRef } from 'react' +import Image from 'next/image' +import axios from 'axios' +import { useSession } from 'next-auth/react' +const page = () => { + const { data: session } = useSession(); + // 제목 + const titleRef = useRef(null); + + // 내용 + const contentRef = useRef(null); + + // 날씨 + const weatherRef = useRef(null); + + // 이미지 제목 + const imgTitRef = useRef(null); + + // 이미지 + const imgRef = useRef(null); + + // 이미지 주소 (미리보기 보여주기 위함) + const [imgUrl, setImgUrl] = useState(""); + + const handleImageView = (e: React.ChangeEvent<{files: FileList | null}>) => { + if (e.target.files && e.target.files.length > 0) { + const file = e.target.files[0]; + URL.revokeObjectURL(imgUrl); + // 이전 미리보기를 지움. + + setImgUrl(prev => URL.createObjectURL(file)); + } + } + const imgReset = () => { + // 미리보기 안 보이게. + if (imgRef.current) { + imgRef.current.value = ''; + URL.revokeObjectURL(imgUrl); + setImgUrl(prev => ''); + } + } + const send = async() => { + if(!session) return; + // 작성. + if(!titleRef.current) { + alert("제목 입력."); + return; + } + if(!contentRef.current) { + alert("내용 입력."); + return; + } + if(!weatherRef.current) { + alert('날씨 입력.'); + return; + } + const formData = new FormData(); + formData.append("title", titleRef.current.value); + formData.append("content", contentRef.current.value); + formData.append('id', session?.user?.id as string); + formData.append('name', session.user?.name as string); + formData.append('weather', weatherRef.current.value); + if(imgRef.current && imgRef.current.files && imgRef.current.files.length > 0 && imgTitRef.current) { + formData.append('img', imgRef.current.files[0]) + formData.append('imgTit', imgTitRef.current.value) + }; + const result = await axios.post( + "/api/diary", + formData, + { + headers: { + "Content-Type":"multipart/form-data", + "Authorization":`mlru ${session?.accessToken}` + } + } + ); + console.log(result.data); + imgReset(); + }; + return ( +
+ 일기 작성 테스트
+ +
+
+ +
+ +
+ +
+ handleImageView(e)}/> +
+ { + imgUrl && ( + <> + preview + +
+ + ) + } +
+ +
+ ) +} + +export default page diff --git a/src/app/icontest/formtest/page.tsx b/src/app/icontest/formtest/page.tsx new file mode 100644 index 0000000..e28f234 --- /dev/null +++ b/src/app/icontest/formtest/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import React, { useState, useRef } from 'react' +import Image from 'next/image' +import axios from 'axios' +// 이미지 여러 개 올리기 하자... +const page = () => { + const [images, setImages] = useState([]); + const [previews, setPreviews] = useState([]); + const imgRef = useRef(null); + + const handleImageView = (e: React.ChangeEvent) => { + const newImages = [...images]; + const newPreviews = [...previews]; + for (let i=0; i < e.target.files!.length; i++) { + if (newImages.length < 3) { + const file = e.target.files![i]; + newImages.push(file) + const reader = new FileReader(); + reader.onload = (e) => { + newPreviews.push(e.target!.result as string); + setPreviews(newPreviews); + }; + reader.readAsDataURL(file); + } + } + setImages(newImages); + }; + + const handleDeletePreview = (index: number) => { + const newImages = [...images]; + const newPreviews = [...previews]; + newImages.splice(index, 1); + newPreviews.splice(index, 1); + setImages(newImages); + setPreviews(newPreviews); + }; + + const send = async() => { + const formData = new FormData(); + images.forEach((img) => { + if( img instanceof File && img.size > 0) { + formData.append('img', img) + }; + }); + const result = await axios.put( + '/api/diary', + formData, + { + headers: { + 'Content-Type':'multipart/form-data' + } + } + ); + console.log(result); + }; + return ( +
+ { + previews?.map((prev, index) =>( +
+ {`${prev}-${index}`} + +
+ )) + } + handleImageView(e)} + /> + +
+ ); +} + +export default page diff --git a/src/app/icontest/icon.css b/src/app/icontest/icon.css new file mode 100644 index 0000000..286b9b0 --- /dev/null +++ b/src/app/icontest/icon.css @@ -0,0 +1,46 @@ +.test { + width: 100%; + height: 100%; + padding: 20px; +} +.btn { + position: relative; + width: 120px; + height: 55px; + border: 1px solid black; + border-radius: 10px; + padding: 7px; + display: flex; + align-items: center; + cursor: pointer; +} +.btn .circle { + position: absolute; + width: 35px; + height: 35px; + border-radius: 50%; + background-color: rgb(222, 222, 207); + top: 50%; + left: 5px; + transform: translateY(-50%); + transition: all 200ms; +} +.btn .circle::after { + display: inline; + content:''; + position: absolute; + width: 30px; + height: 30px; + border-radius: 50%; + background-color: white; + top:-5px; + left:10px; +} + +.btn.toggle .circle { + left: 75px; + background-color: orange; +} +.btn.toggle .circle::after { + display: none; +} \ No newline at end of file diff --git a/src/app/icontest/page.tsx b/src/app/icontest/page.tsx new file mode 100644 index 0000000..a35ffed --- /dev/null +++ b/src/app/icontest/page.tsx @@ -0,0 +1,19 @@ +"use client" + + +import { useState } from 'react'; +import './icon.css'; +const Page = () => { + const [toggle, setToggle] = useState(false); + return ( +
+
setToggle(!toggle)}> +
+ +
+
+
+ ) +}; + +export default Page; \ No newline at end of file diff --git a/src/app/lib/atoms/atom.ts b/src/app/lib/atoms/atom.ts new file mode 100644 index 0000000..b4e8a4d --- /dev/null +++ b/src/app/lib/atoms/atom.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil' + +export const textState = atom({ + key: 'textState', // unique ID (with respect to other atoms/selectors) + default: 'test test test', // default value (aka initial value) + }); \ No newline at end of file diff --git a/src/app/lib/jwt.ts b/src/app/lib/jwt.ts index c786cf2..2ac65e5 100644 --- a/src/app/lib/jwt.ts +++ b/src/app/lib/jwt.ts @@ -36,11 +36,12 @@ export const signJwtRefreshToken = async(user_id: string) => { export const verifyJwt = (token: string) => { try { const secret_key = process.env.SECRET_KEY - const decoded = jwt.verify(token, secret_key!) + const decoded = jwt.verify(token, secret_key!); + const currentTime = Math.floor(Date.now() / 1000) - 60000; // 갱신할 시간. return decoded as JwtPayload } catch(err) { - console.log(err); - return null; + console.log('에러에러에러'); + return "signout"; } } diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx deleted file mode 100644 index b3d1d6d..0000000 --- a/src/app/signin/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import LoginPage from "./_components/LoginPage"; - -const Login = () => { - return ( -
- -
- ); -}; - -export default Login; diff --git a/src/app/signin/_components/LoginPage.tsx b/src/app/signin2/_components/LoginPage.tsx similarity index 97% rename from src/app/signin/_components/LoginPage.tsx rename to src/app/signin2/_components/LoginPage.tsx index a77f867..e15b76b 100644 --- a/src/app/signin/_components/LoginPage.tsx +++ b/src/app/signin2/_components/LoginPage.tsx @@ -1,39 +1,39 @@ -"use client"; - -import type { NextPage } from "next"; -import { useRef } from "react"; -const Login: NextPage = () => { - // useRef로 아이디랑, 비밀번호 값 가져오기. - // axios post 방식으로 전달해 주기 => 주소: http://localhost:3000/api/login , {username: id, password: password} - // no user나 wrong password => 로그인 실패 - - // 로그인이 성공하면 알아서 홈으로 이동됨. - - // id: test1 pw: 1234 - return ( -
-
- Relu molu -
-
- 아이디 - -
-
- 비밀번호 - -
-
- -
-
- - Relumolu@googlo.com -
-
- ); -}; - -export default Login; +"use client"; + +import type { NextPage } from "next"; +import { useRef } from "react"; +const Login: NextPage = () => { + // useRef로 아이디랑, 비밀번호 값 가져오기. + // axios post 방식으로 전달해 주기 => 주소: http://localhost:3000/api/login , {username: id, password: password} + // no user나 wrong password => 로그인 실패 + + // 로그인이 성공하면 알아서 홈으로 이동됨. + + // id: test1 pw: 1234 + return ( +
+
+ Relu molu +
+
+ 아이디 + +
+
+ 비밀번호 + +
+
+ +
+
+ + Relumolu@googlo.com +
+
+ ); +}; + +export default Login; diff --git a/src/app/signin2/page.tsx b/src/app/signin2/page.tsx new file mode 100644 index 0000000..8cc0b8d --- /dev/null +++ b/src/app/signin2/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { signIn, signOut, useSession } from 'next-auth/react' +import { useRef, useState } from 'react' +import axios from 'axios' +import LoginPage from "./_components/LoginPage"; + +const Page = () => { + const idRef = useRef(null); + const pwRef = useRef(null); + const textRef = useRef(null); + const { data: session } = useSession(); + + const handleSubmit = async () => { + if (!idRef.current && !pwRef.current) return null; + const user_id = idRef.current?.value; + const password = pwRef.current?.value; + + const result = await signIn("credentials", { + username: user_id, + password: password, + redirect: false, + callbackUrl: '/' + }); + console.log(result); + }; + const handleSingOut = async () => { + if (session?.user?.provider === 'kakao') { + const result = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/api/logout/kakao`, { + snsAccess: session?.snsAccess + }, { + headers: { + 'Authorization': `mlru ${session.accessToken}` + } + }); + const rst = result.data; + if (rst.result === 'ok') { + // 연결 끊기 성공. + await signOut(); + alert('카카오 로그아웃(연결 끊기) 성공.'); + } else { + // 에러. + console.log(rst.result); + } + } else if (session?.user?.provider === 'google') { + const result = await axios.post(`${process.env.NEXT_PUBLIC_BASE_URL}/api/logout/google`, { + snsAccess: session?.snsAccess + }, { + headers: { + 'Authorization': `mlru ${session.accessToken}` + } + }); + const rst = result.data; + if (rst.result === 'ok') { + // 연결 끊기 성공. + await signOut(); + alert('구글 로그아웃(연동 해제) 성공.'); + } else { + // 에러. + console.log(rst.result); + } + } + } + return ( +
+ {/* */} + + +