diff --git a/.env.development b/.env.development index fb9112b..4e7f5a9 100644 --- a/.env.development +++ b/.env.development @@ -13,8 +13,8 @@ NEXT_PUBLIC_INQUIRY_API_URL=http://172.30.1.93:8120 #QPARTNER 로그인 api -DB_HOST=asdf -DB_USER=asdf -DB_PASSWORD=asdf -DB_DATABASE=asdf +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners DB_PORT=3306 \ No newline at end of file diff --git a/.env.production b/.env.production index ab8cca4..f741721 100644 --- a/.env.production +++ b/.env.production @@ -10,8 +10,8 @@ NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 #QPARTNER 로그인 api -DB_HOST=asdf -DB_USER=asdf -DB_PASSWORD=asdf -DB_DATABASE=asdf +DB_HOST=202.218.61.226 +DB_USER=readonly +DB_PASSWORD=aAjmFW12iHKW84l1 +DB_DATABASE=qpartners DB_PORT=3306 \ No newline at end of file diff --git a/README.md b/README.md index 950676f..6d332e0 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,25 @@ session에 있는 role 키로 구분한다 session.role === 'Admin_Sub' - constA03_01 / 1234 -> 시공사\ session.role === 'Builder' +- teshg44 / 1234 -> 시공사\ + session.role === 'Builder' - partners -> Q.Partners 계정\ session.role === 'Partner' - 이외의 경우 -> 굳이 체크할 필요 없어보임\ session.role === 'User' + +# 지붕재 적합성 TODO + +``` +const suitableCheck = (value: string) => { + if (value === '×') { + return + } else if (value === 'ー') { + return + } else { + return + } + } +``` + +- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 diff --git a/package-lock.json b/package-lock.json index 07a4241..f753e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "@prisma/client": "^6.7.0", "@tanstack/react-query": "^5.71.0", "@tanstack/react-query-devtools": "^5.71.0", + "@types/html-pdf": "^3.0.3", "axios": "^1.8.4", + "html-pdf": "^3.0.1", "iron-session": "^8.0.4", "mssql": "^11.0.1", "next": "15.2.4", @@ -1940,6 +1942,15 @@ "integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==", "license": "MIT" }, + "node_modules/@types/html-pdf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/html-pdf/-/html-pdf-3.0.3.tgz", + "integrity": "sha512-Cw6EpCU5OdSG/yytol7hFNLHxwNoYqOeYL+1GqjhA3YBMJTC8mvT5tFmpLpjrj4WKqe7QoerX4pDwQcXsTotIA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.17.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.28.tgz", @@ -2014,6 +2025,43 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2032,6 +2080,23 @@ "node": ">= 4.5.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT", + "optional": true + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -2072,6 +2137,16 @@ ], "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/bl": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", @@ -2133,12 +2208,29 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "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==", + "license": "MIT", + "optional": true + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2225,6 +2317,13 @@ "license": "MIT", "optional": true }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2312,6 +2411,48 @@ "node": ">=16" } }, + "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" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2333,6 +2474,13 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT", + "optional": true + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -2349,6 +2497,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2449,6 +2610,17 @@ "node": ">= 0.4" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2517,6 +2689,13 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT", + "optional": true + }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", @@ -2589,6 +2768,80 @@ "node": ">=0.8.x" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "optional": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT", + "optional": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -2628,6 +2881,16 @@ } } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -2643,6 +2906,18 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2704,6 +2979,16 @@ "node": ">= 0.4" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2720,9 +3005,34 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2750,6 +3060,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha512-jZ38TU/EBiGKrmyTNNZgnvCZHNowiRI4+w/I9noMlekHTZH3KyGgvJLmhSgykeAQ9j2SYPDosM0Bg3wHfzibAQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2762,6 +3086,22 @@ "node": ">= 0.4" } }, + "node_modules/html-pdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-pdf/-/html-pdf-3.0.1.tgz", + "integrity": "sha512-CKNSacmQn+CKJ2GNfT4UYKaPy/T3Ndj82yJ2aju/UPmnvWNjIpyumqRqkFU0mwT6BTHBFhFGTnXN8dBn4Bdj0Q==", + "deprecated": "Please migrate your projects to a newer library like puppeteer", + "license": "MIT", + "bin": { + "html-pdf": "bin/index.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "optionalDependencies": { + "phantomjs-prebuilt": "^2.1.16" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -2788,6 +3128,22 @@ "node": ">= 14" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2942,6 +3298,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", + "optional": true + }, "node_modules/is-wsl": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", @@ -2957,6 +3330,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", + "optional": true + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -2973,6 +3367,44 @@ "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", "license": "MIT" }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT", + "optional": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT", + "optional": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -3034,6 +3466,22 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -3055,6 +3503,23 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -3386,6 +3851,29 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3525,6 +4013,16 @@ "license": "MIT", "optional": true }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/open": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", @@ -3543,6 +4041,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "optional": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3550,6 +4055,29 @@ "license": "MIT", "optional": true }, + "node_modules/phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==", + "deprecated": "this package is now deprecated", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "bin": { + "phantomjs": "bin/phantomjs" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3569,6 +4097,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -3636,12 +4187,61 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, + "node_modules/progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -3722,6 +4322,75 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha512-dxdraeZVUNEn9AvLrxkgB2k6buTlym71dJk1fk4v8j3Ou3RKNm07BcgbHdj2lLgYGfqX71F+awb1MR+tWPFJzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3865,6 +4534,32 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", @@ -4030,6 +4725,16 @@ "utrie": "^1.0.2" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4043,12 +4748,53 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -4075,6 +4821,16 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/usehooks-ts": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", @@ -4090,6 +4846,13 @@ "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -4108,6 +4871,45 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", diff --git a/package.json b/package.json index 519fd8a..a57feca 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "iron-session": "^8.0.4", "lucide": "^0.503.0", "mssql": "^11.0.1", + "mysql2": "^3.14.1", "next": "15.2.4", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -27,6 +28,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/mysql": "^2.15.27", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1875d7..9d972d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: mssql: specifier: ^11.0.1 version: 11.0.1 + mysql2: + specifier: ^3.14.1 + version: 3.14.1 next: specifier: 15.2.4 version: 15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) @@ -57,6 +60,9 @@ importers: '@tailwindcss/postcss': specifier: ^4 version: 4.0.17 + '@types/mysql': + specifier: ^2.15.27 + version: 2.15.27 '@types/node': specifier: ^20 version: 20.17.28 @@ -676,6 +682,9 @@ packages: '@tediousjs/connection-string@0.5.0': resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@20.17.28': resolution: {integrity: sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==} @@ -712,6 +721,10 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axios@1.8.4: resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} @@ -826,6 +839,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -911,6 +928,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -994,6 +1014,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -1112,6 +1135,17 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide@0.503.0: resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==} @@ -1139,6 +1173,14 @@ packages: engines: {node: '>=18'} hasBin: true + mysql2@3.14.1: + resolution: {integrity: sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1275,6 +1317,9 @@ packages: engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + sharp@0.33.5: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1289,6 +1334,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stackblur-canvas@2.7.0: resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} engines: {node: '>=0.1.14'} @@ -1890,6 +1939,10 @@ snapshots: '@tediousjs/connection-string@0.5.0': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 20.17.28 + '@types/node@20.17.28': dependencies: undici-types: 6.19.8 @@ -1923,6 +1976,8 @@ snapshots: atob@2.1.2: {} + aws-ssl-profiles@1.1.2: {} + axios@1.8.4: dependencies: follow-redirects: 1.15.9 @@ -2041,6 +2096,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + detect-libc@1.0.3: optional: true @@ -2141,6 +2198,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2230,6 +2291,8 @@ snapshots: is-number@7.0.0: optional: true + is-property@1.0.2: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -2346,6 +2409,12 @@ snapshots: lodash.once@4.1.1: {} + long@5.3.2: {} + + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + lucide@0.503.0: {} math-intrinsics@1.1.0: {} @@ -2375,6 +2444,22 @@ snapshots: transitivePeerDependencies: - supports-color + mysql2@3.14.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + nanoid@3.3.11: {} native-duplexpair@1.0.0: {} @@ -2508,6 +2593,8 @@ snapshots: semver@7.7.1: {} + seq-queue@0.0.5: {} + sharp@0.33.5: dependencies: color: 4.2.3 @@ -2544,6 +2631,8 @@ snapshots: sprintf-js@1.1.3: {} + sqlstring@2.3.3: {} + stackblur-canvas@2.7.0: optional: true diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7996ce0..17cc1f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,18 +7,6 @@ datasource db { url = env("DATABASE_URL") } -model User { - id Int @id @default(autoincrement()) - username String @unique - phone String? - email String? - password String? - kakao_id String? - avatar String? - created_at DateTime @default(now()) - updated_at DateTime @updatedAt -} - model MS_SUITABLE { id Int @id @default(autoincrement()) product_name String @db.VarChar(200) @@ -58,21 +46,23 @@ model MS_SUITABLE { } model SD_SURVEY_SALES_BASIC_INFO { - ID Int @id @default(autoincrement()) - REPRESENTATIVE String @db.VarChar(200) - STORE String? @db.VarChar(200) - CONSTRUCTION_POINT String? @db.VarChar(200) - INVESTIGATION_DATE String? @db.VarChar(10) - BUILDING_NAME String? @db.VarChar(200) - CUSTOMER_NAME String? @db.VarChar(200) - POST_CODE String? @db.VarChar(10) - ADDRESS String? @db.VarChar(200) - ADDRESS_DETAIL String? @db.VarChar(300) - SUBMISSION_STATUS Boolean @default(false) - SUBMISSION_DATE DateTime? @db.Date - REG_DT DateTime @default(now()) - UPT_DT DateTime @updatedAt - DETAIL_INFO SD_SURVEY_SALES_DETAIL_INFO? + ID Int @id @default(autoincrement()) + SRL_NO String @db.VarChar(20) + REPRESENTATIVE String @db.VarChar(200) + STORE String? @db.VarChar(200) + CONSTRUCTION_POINT String? @db.VarChar(200) + INVESTIGATION_DATE String? @db.VarChar(10) + BUILDING_NAME String? @db.VarChar(200) + CUSTOMER_NAME String? @db.VarChar(200) + POST_CODE String? @db.VarChar(10) + ADDRESS String? @db.VarChar(200) + ADDRESS_DETAIL String? @db.VarChar(300) + SUBMISSION_STATUS Boolean @default(false) + SUBMISSION_DATE DateTime? @db.Date + SUBMISSION_TARGET_ID String? @db.VarChar(200) + REG_DT DateTime @default(now()) + UPT_DT DateTime @updatedAt + DETAIL_INFO SD_SURVEY_SALES_DETAIL_INFO? } model SD_SURVEY_SALES_DETAIL_INFO { @@ -120,28 +110,28 @@ model SD_SURVEY_SALES_DETAIL_INFO { model BC_COMM_H { HEAD_CD String @id(map: "PK_BC_COMM_H") @db.NVarChar(6) - HEAD_ID String @db.NVarChar(100) - HEAD_NM String @db.NVarChar(100) - HEAD_JP String @db.NVarChar(100) - HEAD_4TH String @db.NVarChar(100) - REF_CHR1 String @db.NVarChar(100) - REF_CHR2 String @db.NVarChar(100) - REF_CHR3 String @db.NVarChar(100) - REF_CHR4 String @db.NVarChar(100) - REF_CHR5 String @db.NVarChar(100) - REF_NUM1 String @db.NVarChar(100) - REF_NUM2 String @db.NVarChar(100) - REF_NUM3 String @db.NVarChar(100) - REF_NUM4 String @db.NVarChar(100) - REF_NUM5 String @db.NVarChar(100) - REMARKS String @db.NVarChar(200) - SAP_YN String @db.NVarChar(1) - STAT_CD String @db.NVarChar(1) - DEL_YN String @db.NVarChar(1) + HEAD_ID String? @db.NVarChar(100) + HEAD_NM String? @db.NVarChar(100) + HEAD_JP String? @db.NVarChar(100) + HEAD_4TH String? @db.NVarChar(100) + REF_CHR1 String? @db.NVarChar(100) + REF_CHR2 String? @db.NVarChar(100) + REF_CHR3 String? @db.NVarChar(100) + REF_CHR4 String? @db.NVarChar(100) + REF_CHR5 String? @db.NVarChar(100) + REF_NUM1 String? @db.NVarChar(100) + REF_NUM2 String? @db.NVarChar(100) + REF_NUM3 String? @db.NVarChar(100) + REF_NUM4 String? @db.NVarChar(100) + REF_NUM5 String? @db.NVarChar(100) + REMARKS String? @db.NVarChar(200) + SAP_YN String? @db.NVarChar(1) + STAT_CD String? @db.NVarChar(1) + DEL_YN String? @db.NVarChar(1) REG_DT DateTime? @db.DateTime - REG_ID String @db.NVarChar(50) + REG_ID String? @db.NVarChar(50) UPT_DT DateTime? @db.DateTime - UPT_ID String @db.NVarChar(50) + UPT_ID String? @db.NVarChar(50) QC_COMM_YN String? @default("N", map: "DF__BC_COMM_H__QC_CO__48CFD27E") @db.NVarChar(1) BC_COMM_L BC_COMM_L[] @@ -179,11 +169,11 @@ model BC_COMM_L { } model MS_SUITABLE_ROOF_MATERIAL_GROUP { - ID Int @id @default(autoincrement()) - ROOF_MATERIAL_GROUP String @db.VarChar(200) - ROOF_MT_CD String @db.VarChar(200) - REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__4F7CD00D") - UPT_DT DateTime + ID Int @id @default(autoincrement()) + ROOF_MATERIAL_GROUP String @db.VarChar(200) + ROOF_MT_CD String @db.VarChar(200) + REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__4F7CD00D") + UPT_DT DateTime? } model MS_SUITABLE_DETAIL { @@ -195,6 +185,8 @@ model MS_SUITABLE_DETAIL { REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__571DF1D5") UPT_DT DateTime? MS_SUITABLE_MAIN MS_SUITABLE_MAIN @relation(fields: [MAIN_ID], references: [ID], onUpdate: NoAction, map: "MS_SUITABLE_DETAIL_MS_SUITABLE_MAIN_FK") + + @@index([MAIN_ID, TRESTLE_MANUFACTURER_PRODUCT_NAME], map: "MS_SUITABLE_DETAIL_MAIN_ID_IDX") } model MS_SUITABLE_MAIN { @@ -206,4 +198,17 @@ model MS_SUITABLE_MAIN { REG_DT DateTime @default(now(), map: "DF__MS_SUITAB__creat__5441852A") UPT_DT DateTime? MS_SUITABLE_DETAIL MS_SUITABLE_DETAIL[] + + @@index([PRODUCT_NAME], map: "MS_SUITABLE_MAIN_PRODUCT_NAME_IDX") + @@index([ROOF_MT_CD, PRODUCT_NAME], map: "MS_SUITABLE_MAIN_ROOF_MT_CD_IDX") +} + +/// The underlying table does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. +model MS_USR_TRK { + ID Int @id @default(autoincrement()) + OWNER String @db.VarChar(100) + TYPE String @db.VarChar(50) + URL String? @db.VarChar(200) + REG_DT DateTime @default(now()) + DATA String? @db.VarChar(200) } diff --git a/src/app/api/auth/chg-pwd/route.ts b/src/app/api/auth/chg-pwd/route.ts index 436f101..71e9f6b 100644 --- a/src/app/api/auth/chg-pwd/route.ts +++ b/src/app/api/auth/chg-pwd/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' - import { axiosInstance } from '@/libs/axios' export async function POST(req: Request) { diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 390f8d1..ad4b365 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -1,13 +1,20 @@ -import { sessionOptions } from '@/libs/session' -import { SessionData } from '@/types/Auth' -import { getIronSession } from 'iron-session' +import type { SessionData } from '@/types/Auth' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' +import { tracking } from '@/libs/tracking' export async function GET(request: Request) { const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) + tracking({ + url: '/api/auth/logout', + data: JSON.stringify({ + userId: session.userId, + }), + }) session.destroy() // return redirect('/login') diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 32ac8f9..d5149a9 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -1,11 +1,10 @@ +import type { SessionData } from '@/types/Auth' import { cookies } from 'next/headers' import { NextResponse } from 'next/server' - import { getIronSession } from 'iron-session' import { axiosInstance } from '@/libs/axios' import { sessionOptions } from '@/libs/session' - -import type { SessionData } from '@/types/Auth' +import { tracking } from '@/libs/tracking' export async function POST(request: Request) { const { loginId, pwd } = await request.json() @@ -14,9 +13,16 @@ export async function POST(request: Request) { loginId, pwd, }) - // console.log('🚀 ~ result ~ result:', result) + console.log('🚀 ~ result ~ result:', result.data) if (result.data.result.code === 200) { + tracking({ + url: `/api/auth/login`, + data: JSON.stringify({ + loginId, + pwd, + }), + }) const cookieStore = await cookies() const session = await getIronSession(cookieStore, sessionOptions) @@ -59,8 +65,6 @@ export async function POST(request: Request) { session.role = 'Admin_Sub' } else if (result.data.data.groupId === '70000' && result.data.data.builderNo !== null) { session.role = 'Builder' - } else if (result.data.data.groupId === '90000' && result.data.data.builderNo !== null) { - session.role = 'Partner' } else { session.role = 'User' } @@ -70,5 +74,50 @@ export async function POST(request: Request) { await session.save() } - return NextResponse.json({ code: 200, message: 'Login is Succecss!!', result: result.data.data }) + const resultForSession = { + LANG_CD: result.data.data.langCd, + CURR_PAGE: result.data.data.currPage, + ROW_COUNT: result.data.data.rowCount, + START_ROW: result.data.data.startRow, + END_ROW: result.data.data.endRow, + COMP_CD: result.data.data.compCd, + AGENCY_STORE_ID: result.data.data.agencyStoreId, + STORE_ID: result.data.data.storeId, + STORE_NM: result.data.data.storeNm, + USER_ID: result.data.data.userId, + CATEGORY: result.data.data.category, + USER_NM: result.data.data.userNm, + USER_NM_KANA: result.data.data.userNmKana, + TEL_NO: result.data.data.telNo, + FAX: result.data.data.fax, + EMAIL: result.data.data.email, + LAST_EDIT_USER: result.data.data.lastEditUser, + STORE_GUBUN: result.data.data.storeGubun, + PW_CURR: result.data.data.pwCurr, + PWD_INIT_YN: result.data.data.pwdInitYn, + APPR_STAT_CD: result.data.data.apprStatCd, + LOGIN_FAIL_CNT: result.data.data.loginFailCnt, + LOGIN_FAIL_MIN_YN: result.data.data.loginFailMinYn, + PRICE_VIEW_STAT_CD: result.data.data.priceViewStatCd, + GROUP_ID: result.data.data.groupId, + STORE_LVL: result.data.data.storeLvl, + CUST_CD: result.data.data.custCd, + BUILDER_NO: result.data.data.builderNo, + IS_LOGGED_IN: true, + ROLE: '', + } + + if (result.data.data.userId === 'T01') { + resultForSession.ROLE = 'T01' + } else if (result.data.data.groupId === '60000') { + resultForSession.ROLE = 'Admin' + } else if (result.data.data.groupId === '70000' && result.data.data.builderNo === null) { + resultForSession.ROLE = 'Admin_Sub' + } else if (result.data.data.groupId === '70000' && result.data.data.builderNo !== null) { + resultForSession.ROLE = 'Builder' + } else { + resultForSession.ROLE = 'User' + } + + return NextResponse.json({ code: 200, message: 'Login is Succecss!!', result: resultForSession }) } diff --git a/src/app/api/comm-code/route.ts b/src/app/api/comm-code/route.ts new file mode 100644 index 0000000..6b4852c --- /dev/null +++ b/src/app/api/comm-code/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import type { CommCode } from '@/types/CommCode' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const headCode = searchParams.get('headCode') + + // @ts-ignore + const headCd = await prisma.BC_COMM_H.findFirst({ + where: { + HEAD_ID: headCode, + }, + select: { + HEAD_CD: true, + }, + }) + + if (!headCd) { + return NextResponse.json({ error: `${headCode}를 찾을 수 없습니다` }, { status: 404 }) + } + + // @ts-ignore + const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({ + where: { + HEAD_CD: headCd.HEAD_CD, + }, + select: { + HEAD_CD: true, + CODE: true, + CODE_JP: true, + }, + orderBy: { + CODE: 'asc', + }, + }) + + return NextResponse.json(roofMaterials) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts new file mode 100644 index 0000000..7d02959 --- /dev/null +++ b/src/app/api/partner/route.ts @@ -0,0 +1,131 @@ +import type { SessionData } from '@/types/Auth' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import { getIronSession } from 'iron-session' +import executeQuery from '@/libs/partner' +import { sessionOptions } from '@/libs/session' + +export async function POST(request: Request) { + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + const { loginId, pwd } = await request.json() + + const sql = ` + SELECT + r.data_id, + u.id AS user_id, + u.login_id AS user_login_id, + u.password AS user_password, + u.user_name AS user_name, + u.user_name_kana AS user_name_kana, + u.sei AS user_sei, + u.mei AS user_mei, + u.sei_kana AS user_sei_kana, + u.mei_kana AS user_mei_kana, + u.user_tel AS user_tel, + u.user_fax AS user_fax, + u.status AS user_status, + u.seko_id AS user_seko_id, + u.seko_limit AS user_seko_limit, + s.id AS supplier_id, + s.code AS supplier_code, + s.name AS supplier_name, + s.name_kana AS supplier_name_kana, + s.kind AS supplier_kind + FROM + R_DATA r + JOIN + M_USER u ON r.data_id = u.id + JOIN + M_SUPPLIER s ON r.relation_id = s.id + WHERE + u.status = '1' + AND + u.seko_id is not null + AND + u.seko_limit > now() + AND + s.kind = '4' + AND + u.login_id = ? + AND + u.password = ? + ` + // const sql = 'SELECT * FROM M_USER' + const data = (await executeQuery(sql, [loginId, pwd])) as any[] + console.log('🚀 ~ POST ~ data:', data) + + if (data.length > 0) { + console.log('start session edit!') + session.langCd = null + session.currPage = null + session.rowCount = null + session.startRow = null + session.endRow = null + session.compCd = null + session.agencyStoreId = null + session.storeId = data[0].supplier_code + session.storeNm = data[0].supplier_name + session.userId = data[0].user_login_id + session.category = data[0].supplier_name + session.userNm = `${data[0].user_sei} ${data[0].user_mei}` + session.userNmKana = `${data[0].user_sei_kana} ${data[0].user_mei_kana}` + session.telNo = data[0].tel + session.fax = data[0].fax + session.email = data[0].user_login_id + session.lastEditUser = null + session.storeGubun = null + session.pwCurr = null + session.pwdInitYn = null + session.apprStatCd = null + session.loginFailCnt = null + session.loginFailMinYn = null + session.priceViewStatCd = null + session.groupId = null + session.storeLvl = null + session.custCd = null + session.builderNo = data[0].user_seko_id + session.isLoggedIn = true + session.role = 'Partner' + + console.log('end session edit!') + + await session.save() + } + + // qsp 유저 데이터 모양과 맞춰서 변환 + const result = { + LANG_CD: null, + CURR_PAGE: null, + ROW_COUNT: null, + START_ROW: null, + END_ROW: null, + COMP_CD: null, + AGENCY_STORE_ID: null, + STORE_ID: data[0].supplier_code, + STORE_NM: data[0].supplier_name, + USER_ID: data[0].user_login_id, + CATEGORY: data[0].supplier_name, + USER_NM: `${data[0].user_sei} ${data[0].user_mei}`, + USER_NM_KANA: `${data[0].user_sei_kana} ${data[0].user_mei_kana}`, + TEL_NO: data[0].tel, + FAX: data[0].fax, + EMAIL: data[0].user_login_id, + LAST_EDIT_USER: null, + STORE_GUBUN: null, + PW_CURR: null, + PWD_INIT_YN: null, + APPR_STAT_CD: null, + LOGIN_FAIL_CNT: null, + LOGIN_FAIL_MIN_YN: null, + PRICE_VIEW_STAT_CD: null, + GROUP_ID: null, + STORE_LVL: null, + CUST_CD: null, + BUILDER_NO: data[0].user_seko_id, + IS_LOGGED_IN: true, + ROLE: 'Partner', + } + + return NextResponse.json({ code: 200, message: 'Partner Login is Succecss!!', result }) +} diff --git a/src/app/api/suitable/category/route.ts b/src/app/api/suitable/category/route.ts deleted file mode 100644 index 288a74a..0000000 --- a/src/app/api/suitable/category/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function GET() { - // @ts-ignore - const roofMaterialCategory = await prisma.MS_SUITABLE.findMany({ - select: { - roof_material: true, - }, - distinct: ['roof_material'], - orderBy: { - roof_material: 'asc', - }, - }) - return NextResponse.json(roofMaterialCategory) -} diff --git a/src/app/api/suitable/detail/route.ts b/src/app/api/suitable/detail/route.ts deleted file mode 100644 index d29f666..0000000 --- a/src/app/api/suitable/detail/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function GET(request: Request) { - const { searchParams } = new URL(request.url) - const roofMaterial = searchParams.get('roof-material') - console.log('🚀 ~ GET ~ roof-material:', roofMaterial) - - // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany({ - where: { - roof_material: roofMaterial, - }, - }) - - return NextResponse.json(suitables) -} diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index d789275..32eac33 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable' export async function GET(request: NextRequest) { try { @@ -7,26 +8,75 @@ export async function GET(request: NextRequest) { const category = searchParams.get('category') const keyword = searchParams.get('keyword') - let whereCondition: any = {} + let MainWhereCondition: any = {} + const whereCondition: string[] = [] + const params: string[] = [] if (category) { - whereCondition['roof_material'] = category + whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`) + params.push(category) + MainWhereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category } if (keyword) { - whereCondition['product_name'] = { + whereCondition.push('PRODUCT_NAME LIKE @P2') + params.push(`%${keyword}%`) + MainWhereCondition['PRODUCT_NAME'] = { contains: keyword, } } - console.log('🚀 ~ /api/suitable/list: ~ prisma where condition:', whereCondition) + const startTime = performance.now() + console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`) // @ts-ignore - const suitables = await prisma.MS_SUITABLE.findMany({ - where: whereCondition, + const suitable = await prisma.MS_SUITABLE_MAIN.findMany({ + select: { + ID: true, + PRODUCT_NAME: true, + ROOF_MT_CD: true, + }, + where: MainWhereCondition, orderBy: { - product_name: 'asc', + PRODUCT_NAME: 'asc', }, }) - return NextResponse.json(suitables) + const endTime = performance.now() + console.log(`쿼리 (main table) 종료 시간: ${endTime - startTime}ms`) + + const mainIds: number[] = suitable.map((item: SuitableMain) => item.id) + + + const startTime2 = performance.now() + console.log(`쿼리 (detail table) 시작 시간: ${startTime2}ms`) + let detailQuery = ` + SELECT + msd.main_id + , ( + SELECT + msd_json.id + , msd_json.trestle_mfpc_cd + , msd_json.trestle_manufacturer_product_name + , msd_json.memo + FROM ms_suitable_detail msd_json + WHERE msd.main_id = msd_json.main_id + FOR JSON PATH + ) AS detail + FROM ms_suitable_detail msd + -- WHERE 1=1 + GROUP BY msd.main_id + ` + if (whereCondition.length > 0) { + detailQuery = detailQuery.replace('-- WHERE 1=1', `WHERE msd.main_id IN @P1`) + } + // @ts-ignore + const detail = await prisma.$queryRawUnsafe(detailQuery, ...mainIds) + + const endTime2 = performance.now() + console.log(`쿼리 (detail table) 종료 시간: ${endTime2 - startTime2}ms`) + + const endTime3 = performance.now() + console.log(`쿼리 총 실행 시간: ${endTime3 - startTime}ms`) + + return NextResponse.json({ suitable, detail }) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) diff --git a/src/app/api/suitable/list/test/route.ts b/src/app/api/suitable/list/test/route.ts new file mode 100644 index 0000000..e4688bd --- /dev/null +++ b/src/app/api/suitable/list/test/route.ts @@ -0,0 +1,71 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/libs/prisma' +import { SUITABLE_HEAD_CODE, type Suitable } from '@/types/Suitable' + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + const whereCondition: string[] = [] + const params: string[] = [] + if (category) { + whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`) + params.push(category) + } + if (keyword) { + whereCondition.push('PRODUCT_NAME LIKE @P2') + params.push(`%${keyword}%`) + } + + const startTime = performance.now() + console.log(`쿼리 시작 시간: ${startTime}ms`) + + let query = ` + SELECT + msm.id + , msm.product_name + , msm.manu_ft_cd + , msm.roof_mt_cd + , msm.roof_sh_cd + , details.detail + FROM ms_suitable_main msm + LEFT JOIN ( + SELECT + msd.main_id + , ( + SELECT + msd_json.id + , msd_json.trestle_mfpc_cd + , msd_json.trestle_manufacturer_product_name + , msd_json.memo + FROM ms_suitable_detail msd_json + WHERE msd.main_id = msd_json.main_id + FOR JSON PATH + ) AS detail + FROM ms_suitable_detail msd + GROUP BY msd.main_id + ) AS details + ON msm.id = details.main_id + -- AND details.main_id IN (#mainIds) + -- WHERE 1=1 + ORDER BY msm.product_name` + + // 검색 조건 추가 + if (whereCondition.length > 0) { + query = query.replace('-- WHERE 1=1', `WHERE ${whereCondition.join(' AND ')}`) + } + + // @ts-ignore + const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, ...params) + + const endTime = performance.now() + console.log(`쿼리 실행 시간: ${endTime - startTime}ms`) + + return NextResponse.json(suitable) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 3da5981..415d1e6 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,9 +1,10 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +import { convertToSnakeCase } from '@/utils/common-utils' -export async function GET(request: Request, context: { params: { id: string } }) { +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findUnique({ where: { ID: Number(id) }, @@ -18,34 +19,34 @@ export async function GET(request: Request, context: { params: { id: string } }) } } -export async function PUT(request: Request, context: { params: { id: string } }) { +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params const body = await request.json() - console.log('body:: ', body) - - // DETAIL_INFO를 분리 const { DETAIL_INFO, ...basicInfo } = body + console.log('body:: ', body) // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ where: { ID: Number(id) }, data: { - ...basicInfo, + ...convertToSnakeCase(basicInfo), UPT_DT: new Date(), - DETAIL_INFO: DETAIL_INFO - ? { - upsert: { - create: DETAIL_INFO, - update: DETAIL_INFO, - }, + DETAIL_INFO: DETAIL_INFO ? { + upsert: { + create: convertToSnakeCase(DETAIL_INFO), + update: convertToSnakeCase(DETAIL_INFO), + where: { + BASIC_INFO_ID: Number(id) } - : undefined, + } + } : undefined }, include: { - DETAIL_INFO: true, - }, + DETAIL_INFO: true + } }) + console.log('survey:: ', survey) return NextResponse.json(survey) } catch (error) { console.error('Error updating survey:', error) @@ -53,9 +54,9 @@ export async function PUT(request: Request, context: { params: { id: string } }) } } -export async function DELETE(request: Request, context: { params: { id: string } }) { +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params await prisma.$transaction(async (tx) => { // @ts-ignore @@ -86,9 +87,9 @@ export async function DELETE(request: Request, context: { params: { id: string } } } -export async function PATCH(request: Request, context: { params: { id: string } }) { +export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { - const { id } = await context.params + const { id } = await params const body = await request.json() if (body.submit) { @@ -98,40 +99,42 @@ export async function PATCH(request: Request, context: { params: { id: string } data: { SUBMISSION_STATUS: true, SUBMISSION_DATE: new Date(), + UPT_DT: new Date(), }, }) return NextResponse.json({ message: 'Survey confirmed successfully' }) - } else { - // @ts-ignore - const hasDetails = await prisma.SD_SURVEY_SALES_DETAIL_INFO.findUnique({ - where: { BASIC_INFO_ID: Number(id) }, - }) - - if (hasDetails) { - //@ts-ignore - const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - UPT_DT: new Date(), - DETAIL_INFO: { - update: body.DETAIL_INFO, - }, - }, - }) - return NextResponse.json(result) - } else { - // @ts-ignore - const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ - where: { ID: Number(id) }, - data: { - DETAIL_INFO: { - create: body.DETAIL_INFO, - }, - }, - }) - return NextResponse.json({ message: 'Survey detail created successfully' }) - } } + // } else { + // // @ts-ignore + // const hasDetails = await prisma.SD_SURVEY_SALES_DETAIL_INFO.findUnique({ + // where: { BASIC_INFO_ID: Number(id) }, + // }) + + // if (hasDetails) { + // //@ts-ignore + // const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + // where: { ID: Number(id) }, + // data: { + // UPT_DT: new Date(), + // DETAIL_INFO: { + // update: convertToSnakeCase(body.DETAIL_INFO), + // }, + // }, + // }) + // return NextResponse.json(result) + // } else { + // // @ts-ignore + // const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ + // where: { ID: Number(id) }, + // data: { + // DETAIL_INFO: { + // create: convertToSnakeCase(body.DETAIL_INFO), + // }, + // }, + // }) + // return NextResponse.json({ message: 'Survey detail created successfully' }) + // } + // } } catch (error) { console.error('Error updating survey:', error) return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 }) diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index d4b676a..3298f5d 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' - +import { convertToSnakeCase } from '@/utils/common-utils' /** * 검색 파라미터 */ @@ -16,7 +16,7 @@ type SearchParams = { } type WhereCondition = { - AND?: any[] + AND: any[] OR?: any[] [key: string]: any } @@ -43,7 +43,7 @@ const ITEMS_PER_PAGE = 10 * @returns 검색 조건 객체 */ const createKeywordSearchCondition = (keyword: string, searchOption: string): WhereCondition => { - const where: WhereCondition = {} + const where: WhereCondition = { AND: [] } if (searchOption === 'all') { // 모든 필드 검색 시 OR 조건 사용 @@ -74,7 +74,6 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh where.ID = { equals: null } } } - return where } @@ -105,21 +104,18 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { AND: [ { STORE: { equals: params.store } }, { - OR: [ - { CONSTRUCTION_POINT: { equals: null } }, - { CONSTRUCTION_POINT: { equals: '' } } - ] - } - ] + OR: [{ CONSTRUCTION_POINT: { equals: null } }, { CONSTRUCTION_POINT: { equals: '' } }], + }, + ], }, { AND: [ { STORE: { equals: params.store } }, { CONSTRUCTION_POINT: { not: null } }, { CONSTRUCTION_POINT: { not: '' } }, - { SUBMISSION_STATUS: { equals: true } } - ] - } + { SUBMISSION_STATUS: { equals: true } }, + ], + }, ] break @@ -159,38 +155,36 @@ export async function GET(request: Request) { } // 검색 조건 구성 - const where: WhereCondition = {} + const where: WhereCondition = { AND: [] } // 내가 작성한 매물 조건 적용 if (params.isMySurvey) { - where.REPRESENTATIVE = params.isMySurvey + where.AND.push({ REPRESENTATIVE: params.isMySurvey }) } // 키워드 검색 조건 적용 if (params.keyword && params.searchOption) { - Object.assign(where, createKeywordSearchCondition(params.keyword, params.searchOption)) + where.AND.push(createKeywordSearchCondition(params.keyword, params.searchOption)) } // 회원 유형 조건 적용 - Object.assign(where, createMemberRoleCondition(params)) - - // 데이터 조회 또는 카운트 - if (params.offset) { - // 페이지네이션 데이터 조회 - //@ts-ignore - const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ - where, - orderBy: params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, - skip: Number(params.offset), - take: ITEMS_PER_PAGE, - }) - return NextResponse.json(surveys) - } else { - // 전체 개수만 조회 - //@ts-ignore - const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) - return NextResponse.json(count) + const roleCondition = createMemberRoleCondition(params) + if (Object.keys(roleCondition).length > 0) { + where.AND.push(roleCondition) } + + // 페이지네이션 데이터 조회 + //@ts-ignore + const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ + where, + orderBy: params.sort === 'created' ? { REG_DT: 'desc' } : { UPT_DT: 'desc' }, + skip: Number(params.offset), + take: ITEMS_PER_PAGE, + }) + // 전체 개수만 조회 + //@ts-ignore + const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) + return NextResponse.json({ data: { data: surveys, count: count } }) } catch (error) { console.error(error) return NextResponse.json({ error: 'Fail Read Survey' }, { status: 500 }) @@ -225,19 +219,22 @@ export async function PUT(request: Request) { } } -export async function POST(request: Request) { +export async function POST(request: Request) { try { const body = await request.json() - const { DETAIL_INFO, ...basicInfo } = body + console.log('body:: ', body) + + const { detailInfo, ...basicInfo } = body + // 기본 정보 생성 //@ts-ignore const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ data: { - ...basicInfo, + ...convertToSnakeCase(basicInfo), DETAIL_INFO: { - create: DETAIL_INFO, - }, - }, + create: convertToSnakeCase(detailInfo) + } + } }) return NextResponse.json(result) } catch (error) { diff --git a/src/app/api/tracking/route.ts b/src/app/api/tracking/route.ts new file mode 100644 index 0000000..913b4c5 --- /dev/null +++ b/src/app/api/tracking/route.ts @@ -0,0 +1,41 @@ +import type { SessionData } from '@/types/Auth' +import { NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import { prisma } from '@/libs/prisma' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' + +export async function POST(request: Request) { + const { url, data } = await request.json() + + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + + let owner = '' + if (url === '/api/auth/login') { + owner = 'Login' + } else if (url === '/api/auth/logout') { + owner = 'Logout' + } else { + owner = session.userId ?? 'Direct' + } + + let type = '' + if (url.includes('api')) { + type = 'api' + } else { + type = 'page' + } + + // @ts-ignore + const result = await prisma.MS_USR_TRK.create({ + data: { + OWNER: owner, + TYPE: type, + URL: url, + DATA: JSON.stringify(data), + }, + }) + + return NextResponse.json({ message: 'Tracking data received', result }) +} diff --git a/src/app/api/user/create/route.ts b/src/app/api/user/create/route.ts deleted file mode 100644 index c4e4060..0000000 --- a/src/app/api/user/create/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export async function POST(request: Request) { - try { - const body = await request.json() - const { username, email, password } = body - - const user = await prisma.user.create({ - data: { - username, - email, - password, - updated_at: new Date(), - }, - }) - - return NextResponse.json(user) - } catch (error) { - console.error('Error creating user:', error) - return NextResponse.json({ error: 'Error creating user' }, { status: 500 }) - } -} diff --git a/src/app/api/user/list/route.ts b/src/app/api/user/list/route.ts deleted file mode 100644 index f84af78..0000000 --- a/src/app/api/user/list/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' - -export const GET = async () => { - const users = await prisma.user.findMany() - return NextResponse.json(users) -} diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts deleted file mode 100644 index 0249fd6..0000000 --- a/src/app/api/user/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextResponse } from 'next/server' -import { prisma } from '@/libs/prisma' -import { getIronSession } from 'iron-session' -import { cookies } from 'next/headers' -import { sessionOptions } from '@/libs/session' -import type { SessionData } from '@/types/Auth' - -export async function POST(request: Request) { - const { username, password } = await request.json() - - console.log('🚀 ~ POST ~ username:', username) - console.log('🚀 ~ POST ~ password:', password) - - const user = await prisma.user.findFirst({ - where: { - username: username, - password: password, - }, - }) - console.log('🚀 ~ POST ~ user:', user) - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }) - } - - const cookieStore = await cookies() - const session = await getIronSession(cookieStore, sessionOptions) - console.log('start session edit!') - // session.username = user.username! - // session.email = user.email! - session.isLoggedIn = true - console.log('end session edit!') - await session.save() - console.log('🚀 ~ POST ~ session:', session) - - // return NextResponse.redirect(new URL(process.env.NEXT_PUBLIC_URL!, request.url)) - return NextResponse.json(user) -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ba05b0..508c340 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,7 +22,7 @@ interface RootLayoutProps { header: ReactNode footer: ReactNode floatBtn: ReactNode -}6 +} export default async function RootLayout({ children, header, footer, floatBtn }: RootLayoutProps): Promise { const cookieStore = await cookies() diff --git a/src/app/pdf/page.tsx b/src/app/pdf/page.tsx new file mode 100644 index 0000000..9a48790 --- /dev/null +++ b/src/app/pdf/page.tsx @@ -0,0 +1,9 @@ +import DownloadPdf from '@/components/DownloadPDF' + +export default function page() { + return ( + <> + + + ) +} diff --git a/src/app/suitable-test/layout.tsx b/src/app/suitable-test/layout.tsx new file mode 100644 index 0000000..e5e7c3f --- /dev/null +++ b/src/app/suitable-test/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +interface SuitableLayoutProps { + children: ReactNode +} + +export default function layout({ children }: SuitableLayoutProps) { + return ( + <> +
+
+
+
+
この適合表は参考資料として使用してください.
+
詳細やお問い合わせは1:1お問い合わせをご利用ください.
+
屋根材の選択or屋根材名を直接入力してください.
+
+
+ {children} +
+
+ + ) +} diff --git a/src/app/suitable-test/page.tsx b/src/app/suitable-test/page.tsx new file mode 100644 index 0000000..a5299fe --- /dev/null +++ b/src/app/suitable-test/page.tsx @@ -0,0 +1,9 @@ +import SuitableRaw from '@/components/suitable/SuitableRaw' + +export default function page() { + return ( + <> + + + ) +} diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index 5ecf1a7..acbb03e 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -1,5 +1,4 @@ import DataTable from '@/components/survey-sale/detail/DataTable' -import DetailForm from '@/components/survey-sale/detail/DetailForm' export default function page() { return ( diff --git a/src/app/survey-sale/basic-info/page.tsx b/src/app/survey-sale/basic-info/page.tsx deleted file mode 100644 index 2c75137..0000000 --- a/src/app/survey-sale/basic-info/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import BasicForm from '@/components/survey-sale/detail/form/BasicForm' - -export default function page() { - return ( - <> - - - ) -} diff --git a/src/app/survey-sale/layout.tsx b/src/app/survey-sale/layout.tsx index f558ad6..83f37c0 100644 --- a/src/app/survey-sale/layout.tsx +++ b/src/app/survey-sale/layout.tsx @@ -9,10 +9,7 @@ export default function layout({ children, navTab }: SurveySaleLayoutProps) { return ( <>
-
- {navTab} - {children} -
+
{children}
) diff --git a/src/app/survey-sale/regist/page.tsx b/src/app/survey-sale/regist/page.tsx index 5090aaa..251d618 100644 --- a/src/app/survey-sale/regist/page.tsx +++ b/src/app/survey-sale/regist/page.tsx @@ -1,9 +1,9 @@ -import RegistForm from '@/components/survey-sale/temp/registForm' +import DetailForm from "@/components/survey-sale/detail/DetailForm"; export default function RegistPage() { return ( <> - + ) } diff --git a/src/app/survey-sale/roof-info/page.tsx b/src/app/survey-sale/roof-info/page.tsx deleted file mode 100644 index 1c5358b..0000000 --- a/src/app/survey-sale/roof-info/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import RoofInfoForm from '@/components/survey-sale/detail/form/RoofInfoForm' - -export default function page() { - return ( - <> - - - ) -} \ No newline at end of file diff --git a/src/components/DownloadPDF.tsx b/src/components/DownloadPDF.tsx new file mode 100644 index 0000000..570b926 --- /dev/null +++ b/src/components/DownloadPDF.tsx @@ -0,0 +1,748 @@ +'use client' + +import { useRef } from 'react' +import generatePDF, { Margin, Resolution } from 'react-to-pdf' + +export default function DownloadPdf() { + const targetRef = useRef(null) + const handleDownPdf = () => { + const options = { + method: 'open' as const, + resolution: Resolution.HIGH, + page: { + margin: Margin.SMALL, + format: 'A4', + orientation: 'portrait' as const, + }, + canvas: { + mimeType: 'image/png' as const, + qualityRatio: 1, + }, + overrides: { + pdf: { + compress: true, + }, + canvas: { + useCORS: true, + }, + }, + } + + generatePDF(targetRef, options) + // generatePDF(targetRef, { filename: 'page.pdf' }) + } + return ( + <> + +
+
+
+
+ HWJ 現地調査シート1/2 +
+
+
+

+ 現地明登施工店名 +

+

+ Sheet2 No.4Sheet2 +

+
+
+

現地阴買日

+

2025.05.09

+
+
+
+
+ + + + + + + + + + + +
+ お客様名 + + Sheet2No.1 +
+ ご住所 + + Sheet2No.2 +
+
+
+
も気開係
+ + + + + + + + + + + + + + + + + +
+ 雨気契约容国 + + Sheet2No.1 + + 電気契約会社 + + Sheet2No.1 +
+ 電気付带設備 + + Sheet2No.7 選式回答表示/自由入力回答表示 +
+ 設置希望システム + + Sheet2No.8 選択式回表示/自由入力回表 +
+
+
+
屋根眀係
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 築年数 + + No.9 + + 至根材 + + No.10 + + 座根形状 + + No.11 +
+ 座根勾配 + + No.12 + + 住宅樠造 + + No.13 +
+ 並木材質 + + No.14 + + 垂木サイズ + + No.15 +
+ 垂木ビッチ + + No.16 + + 垂木方向 + + No.17 +
+ 野地板種類 + + No.18 + + 野地板厚さ + + No.19 +
+ 兩漏の形跡 + + No.20 +
+ ルーフィング種類 + + No.21 +
+ 断熱材の有無 + + No.22 +
+ 屋根構造の順番 + + No.23 +
+
+
+ + + + + + + +
+ 区根製品名設置可否確認 + + No.23 +
+
+
+
メモ
+
+ No.25 +
+
+
+
+ + ) +} diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 5f086af..6322ec4 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -3,10 +3,8 @@ import type { SessionData } from '@/types/Auth' import { useEffect, useReducer, useState } from 'react' import { useRouter } from 'next/navigation' - import { useLocalStorage } from 'usehooks-ts' import { useQuery } from '@tanstack/react-query' - import { axiosInstance } from '@/libs/axios' import { useSessionStore } from '@/store/session' @@ -35,6 +33,11 @@ export default function Login() { pwd: '', }) + const isValidEmail = (email: string) => { + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + return emailRegex.test(email) + } + interface LoginData { code: number message: string | null @@ -48,7 +51,14 @@ export default function Login() { } = useQuery({ queryKey: ['login', 'account'], queryFn: async () => { - const { data } = await axiosInstance('').post(`/api/auth`, { + let url = '' + if (!isPartners) { + url = '/api/auth' + } else { + url = '/api/partner' + } + + const { data } = await axiosInstance('').post(`${url}`, { loginId: account.loginId, pwd: account.pwd, }) @@ -68,6 +78,7 @@ export default function Login() { indivisualData: account.pwd, }) // 세션 정보 저장 + console.log('🚀 ~ Login ~ loginData:', loginData) setSession({ ...session, ...loginData?.result, @@ -76,6 +87,14 @@ export default function Login() { } }, [loginData]) + useEffect(() => { + if (isValidEmail(account.loginId)) { + setIsPartners(true) + } else { + setIsPartners(false) + } + }, [account.loginId]) + return ( <>
@@ -88,7 +107,7 @@ export default function Login() { value={account.loginId} onChange={(e) => setAccount({ loginId: e.target.value })} /> -
diff --git a/src/components/popup/MemberInformationPopup.tsx b/src/components/popup/MemberInformationPopup.tsx index 87a8829..8cf0bae 100644 --- a/src/components/popup/MemberInformationPopup.tsx +++ b/src/components/popup/MemberInformationPopup.tsx @@ -57,9 +57,11 @@ export default function MemberInformationPopup() { - + {session.role !== 'Partner' && ( + + )} diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index f9f6615..36a397f 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -1,28 +1,70 @@ 'use client' -import { useState } from 'react' -import SuitableCheckData from './SuitableCheckData' -import SuitableNoData from './SuitableNoData' import Image from 'next/image' +import { useEffect, useState } from 'react' +import SuitableList from './SuitableList' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' +import type { CommCode } from '@/types/CommCode' +import { SUITABLE_HEAD_CODE } from '@/types/Suitable' export default function Suitable() { - const [reference, setReference] = useState(false) + const [reference, setReference] = useState(true) + + const { getSuitableCommCode, refetchBySearch } = useSuitable() + const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() + + const handleInputSearch = async () => { + if (!searchValue.trim()) { + alert('屋根材の製品名を入力してください。') + return + } + setIsSearch(true) + refetchBySearch() + } + + const handleInputClear = () => { + setSearchValue('') + setIsSearch(false) + refetchBySearch() + } + + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + useEffect(() => { + getSuitableCommCode() + return () => { + setSelectedCategory('') + setSearchValue('') + clearSelectedItems() + } + }, []) return (
- setSelectedCategory(e.target.value)}> + {suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => ( + + ))}
- - + setSearchValue(e.target.value)} + /> + {searchValue &&
@@ -68,37 +110,8 @@ export default function Suitable() {
- {/* checkData */} - {/* 데이터 없을경우 버튼 영역 안보여야함 */} - - - - - - {/* 데이터 없을경우 버튼 영역 안보여야함 */} -
-
-
- -
-
- -
-
- -
-
-
+
- - {/* 검색기록 없을떄 위에 두 영역 안보이고 이 부분만 보여야 함*/} - {/* */} ) } diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx new file mode 100644 index 0000000..f412c89 --- /dev/null +++ b/src/components/suitable/SuitableButton.tsx @@ -0,0 +1,25 @@ +'use client' + +export default function SuitableButton() { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/components/suitable/SuitableCheckData.tsx b/src/components/suitable/SuitableCheckData.tsx deleted file mode 100644 index 2a57c21..0000000 --- a/src/components/suitable/SuitableCheckData.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client' - -import Image from 'next/image' - -export default function SuitableCheckData() { - return ( - <> -
-
-
- - -
-
- -
-
-
    -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
-
- - ) -} diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx new file mode 100644 index 0000000..18d94e5 --- /dev/null +++ b/src/components/suitable/SuitableList.tsx @@ -0,0 +1,174 @@ +'use client' + +import Image from 'next/image' +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import SuitableButton from './SuitableButton' +import SuitableNoData from './SuitableNoData' +import { useSuitable } from '@/hooks/useSuitable' +import { useSuitableStore } from '@/store/useSuitableStore' +import { SUITABLE_HEAD_CODE, type SuitableMain, type SuitableDetail } from '@/types/Suitable' + +// 한 번에 로드할 아이템 수 +const ITEMS_PER_PAGE = 100 + +export default function SuitableList() { + const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + + const [openItems, setOpenItems] = useState>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(null) + + // 선택된 아이템 확인 함수 메모이제이션 + const isItemSelected = useCallback( + (itemId: number) => { + return selectedItems.some((selected) => selected === itemId) + }, + [selectedItems], + ) + + // 초기 데이터 로드 + useEffect(() => { + if (suitableSearchResults) { + const initialItems = suitableSearchResults.suitable.slice(0, ITEMS_PER_PAGE) + setVisibleItems(initialItems) + setPage(1) + } + }, [suitableSearchResults]) + + // Intersection Observer 설정 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) { + const nextPage = page + 1 + const startIndex = (nextPage - 1) * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + const nextItems = suitableSearchResults.suitable.slice(startIndex, endIndex) + + if (nextItems.length > 0) { + setIsLoadingMore(true) + setVisibleItems((prev) => [...prev, ...nextItems]) + setPage(nextPage) + setIsLoadingMore(false) + } + } + }, + { + threshold: 0.2, + }, + ) + + if (observerTarget.current) { + observer.observe(observerTarget.current) + } + + return () => observer.disconnect() + }, [page, suitableSearchResults, isLoadingMore]) + + const handleItemClick = useCallback( + (itemId: number) => { + isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) + }, + [isItemSelected, addSelectedItem, removeSelectedItem], + ) + + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheck = useCallback((value: string) => { + if (value === '×') { + return ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: SuitableMain) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.id).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.suitable.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableListRaw.tsx b/src/components/suitable/SuitableListRaw.tsx new file mode 100644 index 0000000..6dc7f36 --- /dev/null +++ b/src/components/suitable/SuitableListRaw.tsx @@ -0,0 +1,173 @@ +'use client' + +import Image from 'next/image' +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' +import SuitableButton from './SuitableButton' +import SuitableNoData from './SuitableNoData' +import { useSuitableRaw, type Suitable } from '@/hooks/useSuitableRaw' +import { useSuitableStore } from '@/store/useSuitableStore' +import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable' + +// 한 번에 로드할 아이템 수 +const ITEMS_PER_PAGE = 100 + +export default function SuitableListRaw() { + const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitableRaw() + const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() + const [openItems, setOpenItems] = useState>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(null) + + // 선택된 아이템 확인 함수 메모이제이션 + const isItemSelected = useCallback( + (itemId: number) => { + return selectedItems.some((selected) => selected === itemId) + }, + [selectedItems], + ) + + // 초기 데이터 로드 + useEffect(() => { + if (suitableSearchResults) { + const initialItems = suitableSearchResults.slice(0, ITEMS_PER_PAGE) + setVisibleItems(initialItems) + setPage(1) + } + }, [suitableSearchResults]) + + // Intersection Observer 설정 + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) { + const nextPage = page + 1 + const startIndex = (nextPage - 1) * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + const nextItems = suitableSearchResults.slice(startIndex, endIndex) + + if (nextItems.length > 0) { + setIsLoadingMore(true) + setVisibleItems((prev) => [...prev, ...nextItems]) + setPage(nextPage) + setIsLoadingMore(false) + } + } + }, + { + threshold: 0.2, + }, + ) + + if (observerTarget.current) { + observer.observe(observerTarget.current) + } + + return () => observer.disconnect() + }, [page, suitableSearchResults, isLoadingMore]) + + const handleItemClick = useCallback( + (itemId: number) => { + isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId) + }, + [isItemSelected, addSelectedItem, removeSelectedItem], + ) + + const toggleItemOpen = useCallback((itemId: number) => { + setOpenItems((prev) => { + const newOpenItems = new Set(prev) + newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId) + return newOpenItems + }) + }, []) + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheck = useCallback((value: string) => { + if (value === '×') { + return ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: Suitable) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableRaw.tsx b/src/components/suitable/SuitableRaw.tsx new file mode 100644 index 0000000..d48dfea --- /dev/null +++ b/src/components/suitable/SuitableRaw.tsx @@ -0,0 +1,118 @@ +'use client' + +import Image from 'next/image' +import { useEffect, useState } from 'react' +import SuitableListRaw from './SuitableListRaw' +import { useSuitableRaw } from '@/hooks/useSuitableRaw' +import { useSuitableStore } from '@/store/useSuitableStore' +import type { CommCode } from '@/types/CommCode' +import { SUITABLE_HEAD_CODE } from '@/types/Suitable' + +export default function SuitableRaw() { + const [reference, setReference] = useState(true) + + const { getSuitableCommCode, refetchBySearch } = useSuitableRaw() + const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() + + const handleInputSearch = async () => { + if (!searchValue.trim()) { + alert('屋根材の製品名を入力してください。') + return + } + setIsSearch(true) + refetchBySearch() + } + + const handleInputClear = () => { + setSearchValue('') + setIsSearch(false) + refetchBySearch() + } + + useEffect(() => { + refetchBySearch() + }, [selectedCategory]) + + useEffect(() => { + getSuitableCommCode() + return () => { + setSelectedCategory('') + setSearchValue('') + clearSelectedItems() + } + }, []) + + return ( +
+ 테스트1 페이지 +
+ +
+
+
+ setSearchValue(e.target.value)} + /> + {searchValue &&
+
+
+
+
+
凡例
+
+ +
+
+
    +
  • +
    +
    + +
    + 設置可能 +
    +
  • +
  • +
    +
    + +
    + 設置不可 +
    +
  • +
  • +
    +
    + +
    + お問い合わせ +
    +
  • +
  • +
    +
    + +
    + 備考 +
    +
  • +
+
+ +
+
+ ) +} diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx new file mode 100644 index 0000000..0942abb --- /dev/null +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -0,0 +1,165 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useSurveySaleTabState } from '@/store/surveySaleTabState' +import type { SurveyBasicRequest } from '@/types/Survey' +import type { Mode } from 'fs' +import { useSessionStore } from '@/store/session' +import { usePopupController } from '@/store/popupController' +import { useAddressStore } from '@/store/addressStore' + +export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBasicInfo: (basicInfo: SurveyBasicRequest) => void; mode: Mode }) { + const { basicInfo, setBasicInfo, mode } = props + const { setBasicInfoSelected } = useSurveySaleTabState() + const [isFlip, setIsFlip] = useState(true) + + const { session } = useSessionStore() + const { addressData } = useAddressStore() + + useEffect(() => { + setBasicInfoSelected() + }, []) + + useEffect(() => { + if (session?.isLoggedIn) { + setBasicInfo({ + ...basicInfo, + representative: session.userNm ?? '', + store: session.storeNm ?? null, + constructionPoint: session.builderNo ?? null, + }) + } + if (addressData) { + setBasicInfo({ + ...basicInfo, + postCode: addressData.post_code, + address: addressData.address, + addressDetail: addressData.address_detail, + }) + } + }, [session, addressData]) + + const popupController = usePopupController() + + return ( + <> +
+
setIsFlip(!isFlip)}> +
基本情報
+
+ +
+
+
+
+
+
+
担当者名
+ setBasicInfo({ ...basicInfo, representative: e.target.value })} + /> +
+ {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( +
+
販売店
+ setBasicInfo({ ...basicInfo, store: e.target.value })} + /> +
+ )} + {(session?.role === 'Builder' || session?.role === 'Partner') && ( +
+
施工店
+ setBasicInfo({ ...basicInfo, constructionPoint: e.target.value })} + /> +
+ )} +
+
+
+
+
+
現地調査日
+ {['CREATE', 'EDIT'].includes(mode as 'CREATE' | 'EDIT') ? ( +
+ + setBasicInfo({ ...basicInfo, investigationDate: e.target.value })} + /> +
+ ) : ( + + )} +
+
+ {/* 건물명 */} +
建物名
+ setBasicInfo({ ...basicInfo, buildingName: e.target.value })} + /> +
+
+ {/* 고객명 */} +
お客様名
+ setBasicInfo({ ...basicInfo, customerName: e.target.value })} + /> +
+
+
郵便番号/都道府県
+
+ {/* 우편번호 */} +
+ +
+ {/* 도도부현 */} +
+ +
+
+ {/* 주소 */} +
+ +
+
+ +
+
市区町村名, 以後の住所
+ +
+
+
+
+
+ + ) +} diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx new file mode 100644 index 0000000..a22ac5c --- /dev/null +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -0,0 +1,267 @@ +'use client' + +import type { Mode, SurveyBasicRequest, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' +import { useSessionStore } from '@/store/session' +import { useEffect, useState } from 'react' +import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { requiredFields, useServey } from '@/hooks/useSurvey' + +export default function ButtonForm(props: { + mode: Mode + setMode: (mode: Mode) => void + data: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } +}) { + // 라우터 + const router = useRouter() + const { mode, setMode } = props + const { session } = useSessionStore() + + const searchParams = useSearchParams() + const idParam = searchParams.get('id') + + const params = useParams() + const routeId = params.id + + const [isSubmitProcess, setIsSubmitProcess] = useState(false) + // ------------------------------------------------------------ + // 권한 + + // 제출권한 ㅇ + const [isSubmiter, setIsSubmiter] = useState(false) + // 작성자 + const [isWriter, setIsWriter] = useState(false) + const isSubmit = props.data.basic.submissionStatus + + useEffect(() => { + if (session?.isLoggedIn) { + setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) + setIsWriter(session.userNm === props.data.basic.representative) + } + }, [session, props.data]) + + // ------------------------------------------------------------ + // 저장/임시저장/수정 + + const id = routeId ? Number(routeId) : Number(idParam) + const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id)) + const { validateSurveyDetail, createSurvey } = useServey() + let saveData = { + ...props.data.basic, + detailInfo: props.data.roof, + } + + const handleSave = (isTemporary: boolean) => { + const emptyField = validateSurveyDetail(props.data.roof) + console.log('handleSave, emptyField:: ', emptyField) + if (isTemporary) { + tempSaveProcess() + } else { + saveProcess(emptyField) + } + } + + const tempSaveProcess = async () => { + if (idParam) { + await updateSurvey(saveData) + router.push(`/survey-sale/detail?id=${idParam}&isTemporary=true`) + } else { + const id = await createSurvey(saveData) + router.push(`/survey-sale/detail?id=${id}&isTemporary=true`) + } + alert('一時保存されました。') + } + + const focusInput = (field: keyof SurveyDetailInfo) => { + const input = document.getElementById(field) + if (input) { + input.focus() + } + } + + const saveProcess = async (emptyField: string) => { + if (emptyField.trim() === '') { + if (idParam) { + // 수정 페이지에서 작성 후 제출 + if (isSubmitProcess) { + saveData = { + ...saveData, + submissionStatus: true, + submissionDate: new Date().toISOString(), + } + } + await updateSurvey(saveData) + router.push(`/survey-sale/${idParam}`) + } else { + const id = await createSurvey(saveData) + if (isSubmitProcess) { + submitProcess(id) + return + } + router.push(`/survey-sale/${id}`) + } + alert('保存されました。') + } else { + if (emptyField.includes('Unit')) { + alert('電気契約容量の単位を入力してください。') + focusInput(emptyField as keyof SurveyDetailInfo) + } else { + alert(requiredFields.find((field) => field.field === emptyField)?.name + ' 項目が空です。') + focusInput(emptyField as keyof SurveyDetailInfo) + } + } + } + // ------------------------------------------------------------ + // 삭제/제출 + + const handleDelete = async () => { + if (routeId) { + window.neoConfirm('削除しますか?', async () => { + await deleteSurvey() + router.push('/survey-sale') + }) + } + } + + const handleSubmit = async () => { + window.neoConfirm('提出しますか?', async () => { + setIsSubmitProcess(true) + if (routeId) { + submitProcess() + } else { + handleSave(false) + } + }) + } + const submitProcess = async (saveId?: number) => { + await submitSurvey(saveId) + alert('提出されました。') + router.push('/survey-sale') + } + // ------------------------------------------------------------ + + if (mode === 'READ' && isSubmit && isSubmiter) { + return ( + <> +
+
+ +
+
+ + ) + } + + return ( + <> + {mode === 'READ' && ( +
+
+ + + {(isWriter || !isSubmiter) && } + {!isSubmit && isSubmiter && } +
+
+ )} + + {(mode === 'CREATE' || mode === 'EDIT') && ( +
+
+ + + + +
+
+ )} + + ) +} + +// 목록 버튼 +function ListButton() { + const router = useRouter() + return ( +
+ {/* 목록 */} + +
+ ) +} + +function EditButton(props: { setMode: (mode: Mode) => void; id: string; mode: Mode }) { + const { setMode, id, mode } = props + const router = useRouter() + return ( +
+ {/* 수정 */} + +
+ ) +} + +function SubmitButton(props: { handleSubmit: () => void }) { + const { handleSubmit } = props + return ( +
+ {/* 제출 */} + +
+ ) +} + +function DeleteButton(props: { handleDelete: () => void }) { + const { handleDelete } = props + return ( +
+ {/* 삭제 */} + +
+ ) +} + +function SaveButton(props: { handleSave: (isTemporary: boolean) => void }) { + const { handleSave } = props + return ( +
+ {/* 저장 */} + +
+ ) +} + +function TempButton(props: { setMode: (mode: Mode) => void; handleSave: (isTemporary: boolean) => void }) { + const { setMode, handleSave } = props + const router = useRouter() + + return ( +
+ {/* 임시저장 */} + +
+ ) +} diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index e7ce554..210d80d 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -4,14 +4,13 @@ import { useServey } from '@/hooks/useSurvey' import { useParams, useSearchParams } from 'next/navigation' import { useEffect, useState } from 'react' import DetailForm from './DetailForm' -import RoofDetailForm from './RoofDetailForm' +import type { SurveyBasicInfo } from '@/types/Survey' export default function DataTable() { const params = useParams() const id = params.id const searchParams = useSearchParams() - const tab = searchParams.get('tab') const isTemp = searchParams.get('isTemporary') const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id)) @@ -20,8 +19,8 @@ export default function DataTable() { const { validateSurveyDetail } = useServey(Number(id)) useEffect(() => { - if (surveyDetail?.DETAIL_INFO) { - const validate = validateSurveyDetail(surveyDetail.DETAIL_INFO) + if (surveyDetail?.detailInfo) { + const validate = validateSurveyDetail(surveyDetail.detailInfo) if (validate.trim() !== '') { setIsTemporary(false) } @@ -34,7 +33,7 @@ export default function DataTable() { return ( <> -
+
@@ -48,25 +47,25 @@ export default function DataTable() { 仮保存 ) : ( - + )} - + - +
{surveyDetail?.ID}{surveyDetail?.id}
登録日{surveyDetail?.REG_DT ? new Date(surveyDetail?.REG_DT).toLocaleString() : ''}{surveyDetail?.regDt ? new Date(surveyDetail.regDt).toLocaleString() : ''}
更新日時{surveyDetail?.UPT_DT ? new Date(surveyDetail?.UPT_DT).toLocaleString() : ''}{surveyDetail?.uptDt ? new Date(surveyDetail.uptDt).toLocaleString() : ''}
提出可否 - {surveyDetail?.SUBMISSION_STATUS && surveyDetail?.SUBMISSION_DATE ? ( + {surveyDetail?.submissionStatus && surveyDetail?.submissionDate ? ( <> {/* TODO: 제출한 판매점 ID 추가 필요 */} -
{new Date(surveyDetail.SUBMISSION_DATE).toLocaleString()}
-
{surveyDetail.STORE}
+
{new Date(surveyDetail.submissionDate).toLocaleString()}
+
{surveyDetail.store}
) : ( '-' @@ -84,11 +83,7 @@ export default function DataTable() {
- {tab === 'roof-info' ? ( - - ) : ( - - )} + ) } diff --git a/src/components/survey-sale/detail/DetailButton.tsx b/src/components/survey-sale/detail/DetailButton.tsx deleted file mode 100644 index 3a871bf..0000000 --- a/src/components/survey-sale/detail/DetailButton.tsx +++ /dev/null @@ -1,119 +0,0 @@ -'use client' -import { useRouter, useSearchParams } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' -import { useSessionStore } from '@/store/session' -import { SurveyBasicInfo } from '@/types/Survey' -import { useState } from 'react' - -export default function DetailButton({ surveyDetail }: { surveyDetail: SurveyBasicInfo | null }) { - const router = useRouter() - const { session } = useSessionStore() - const { submitSurvey, deleteSurvey } = useServey(surveyDetail?.ID ?? 0) - - const searchParams = useSearchParams() - const isTemp = searchParams.get('isTemporary') - const [isTemporary, setIsTemporary] = useState(isTemp === 'true') - - const checkRole = () => { - switch (session?.role) { - case 'T01': - return session?.userNm === surveyDetail?.REPRESENTATIVE ? true : false - case 'Admin': - return session?.storeNm === surveyDetail?.STORE ? true : false - case 'Admin_Sub': - return session?.storeNm === surveyDetail?.STORE ? true : false - case 'Builder': - return session?.builderNo === surveyDetail?.CONSTRUCTION_POINT ? true : false - case 'Partner': - return session?.builderNo === surveyDetail?.CONSTRUCTION_POINT ? true : false - default: - return '' - } - } - - const handleSubmit = async () => { - const result = checkRole() - if (result) { - if (isTemporary) { - alert('一時保存されたデータは提出できません。') - return - } - window.neoConfirm( - '提出しますか??', - async () => { - if (surveyDetail?.ID) { - // TODO: 제출 페이지 추가 - alert('SUBMIT POPUP!!!!!!!!!!!') - await submitSurvey() - } - }, - () => null, - ) - } - } - const handleUpdate = () => { - const result = checkRole() - if (result) { - // router.push(`/survey-sale/basic-info?id=${surveyDetail?.ID}&isTemp=${isTemporary}`) - router.push(`/survey-sale/regist?id=${surveyDetail?.ID}`) - } else { - alert('担当者のみ修正可能です。') - } - } - const handleDelete = async () => { - window.neoConfirm( - '削除しますか?', - async () => { - if (surveyDetail?.ID) { - if (session.userNm === surveyDetail?.REPRESENTATIVE) { - await deleteSurvey() - alert('削除されました。') - router.push('/survey-sale') - } else { - alert('担当者のみ削除可能です。') - } - } - }, - () => null, - ) - } - - const isSubmitter = session?.storeNm === surveyDetail?.STORE && session?.builderNo === surveyDetail?.CONSTRUCTION_POINT - - return ( -
-
- -
- {isSubmitter && surveyDetail?.SUBMISSION_STATUS ? ( - <> - ) : ( - <> - {isTemporary || surveyDetail?.SUBMISSION_STATUS ? ( - <> - ) : ( - <> -
- -
- - )} -
- -
-
- -
- - )} -
- ) -} diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 73ce5c7..0467aec 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -1,53 +1,107 @@ 'use client' -import DetailButton from './DetailButton' -import { SurveyBasicInfo } from '@/types/Survey' +import type { Mode, SurveyBasicInfo, SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey' +import { useEffect, useState } from 'react' +import ButtonForm from './ButtonForm' +import BasicForm from './BasicForm' +import RoofForm from './RoofForm' +import { useParams, useSearchParams } from 'next/navigation' +import { useServey } from '@/hooks/useSurvey' -export default function DetailForm({ - surveyDetail, - isLoadingSurveyDetail, -}: { - surveyDetail: SurveyBasicInfo | null - isLoadingSurveyDetail: boolean -}) { - if (isLoadingSurveyDetail) { - return
Loading...
+const roofInfoForm: SurveyDetailRequest = { + contractCapacity: null, + retailCompany: null, + supplementaryFacilities: null, + supplementaryFacilitiesEtc: null, + installationSystem: null, + installationSystemEtc: null, + constructionYear: null, + constructionYearEtc: null, + roofMaterial: null, + roofMaterialEtc: null, + roofShape: null, + roofShapeEtc: null, + roofSlope: null, + houseStructure: '1', + houseStructureEtc: null, + rafterMaterial: '1', + rafterMaterialEtc: null, + rafterSize: null, + rafterSizeEtc: null, + rafterPitch: null, + rafterPitchEtc: null, + rafterDirection: '1', + openFieldPlateKind: null, + openFieldPlateKindEtc: null, + openFieldPlateThickness: null, + leakTrace: false, + waterproofMaterial: null, + waterproofMaterialEtc: null, + insulationPresence: '1', + insulationPresenceEtc: null, + structureOrder: null, + structureOrderEtc: null, + installationAvailability: null, + installationAvailabilityEtc: null, + memo: null, +} + +const basicInfoForm: SurveyBasicRequest = { + representative: '', + store: null, + constructionPoint: null, + investigationDate: new Date().toLocaleDateString('en-CA'), + buildingName: null, + customerName: null, + postCode: null, + address: null, + addressDetail: null, + submissionStatus: false, + submissionDate: null, +} + +export default function DetailForm() { + const idParam = useSearchParams().get('id') + const routeId = useParams().id + + const id = idParam ?? routeId + + const { surveyDetail } = useServey(Number(id)) + + const [mode, setMode] = useState(idParam ? 'EDIT' : routeId ? 'READ' : 'CREATE') + const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) + const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) + + useEffect(() => { + if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { + const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail + setBasicInfoData(rest) + if (detailInfo) { + const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo + setRoofInfoData(rest) + } + } + }, [surveyDetail, mode]) + + // console.log('mode:: ', mode) + // console.log('surveyDetail:: ', surveyDetail) + // console.log('roofInfoData:: ', roofInfoData) + + const data = { + basic: basicInfoData, + roof: roofInfoData, } + + const buttonFormProps = { mode, setMode, data } + return ( <> -
-
-
-
担当者名
- -
-
-
販売店
- -
-
-
施工店
- -
-
-
- -
-
-
-
現地調査日
- -
-
-
建物名
- -
-
-
顧客名
- -
-
- +
+ {/* 기본정보 */} + + {/* 전기/지붕정보 */} + +
) diff --git a/src/components/survey-sale/detail/RoofDetailForm.tsx b/src/components/survey-sale/detail/RoofDetailForm.tsx deleted file mode 100644 index a9a4031..0000000 --- a/src/components/survey-sale/detail/RoofDetailForm.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { SurveyBasicInfo, SurveyDetailInfo } from '@/types/Survey' -import DetailButton from './DetailButton' -import { roof_material, supplementary_facilities } from './form/etcProcess/MultiCheckEtc' -import { selectBoxOptions } from './form/etcProcess/SelectBoxEtc' -import { radioEtcData } from './form/etcProcess/RadioEtc' - -export default function RoofDetailForm({ - surveyDetail, - isLoadingSurveyDetail, -}: { - surveyDetail: SurveyBasicInfo | null - isLoadingSurveyDetail: boolean -}) { - console.log(surveyDetail) - - const makeNumArr = (value: string) => { - return value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - } - - if (isLoadingSurveyDetail) { - return
Loading...
- } - return ( - <> -
-
-
- {/* 전기 계약 용량 */} -
電気契約容量
- -
- {/* 전기 소매 회사 */} -
-
電気小売会社
- -
- {/* 전기 부대 설비 */} -
-
電気附属設備
-
- {supplementary_facilities.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
-
- {/* 설치 희망 시스템 */} -
-
設置希望システム
- -
- {/* 건축 연수 */} -
-
建築年数
- -
- {/* 지붕재 */} -
-
屋根材
-
- {roof_material.map((item) => ( -
- - -
- ))} -
- - -
-
-
- -
-
- {/* 지붕 모양 */} -
-
屋根の形状
- -
- {/* 지붕 경사도 */} -
-
屋根の斜面
-
- - -
-
- {/* 주택 구조 */} -
-
住宅構造
- -
- {/* 서까래 재질 */} -
-
垂木の材質
- -
- {/* 서까래 크기 */} -
-
垂木の大きさ
- -
- {/* 서까래 피치 */} -
-
垂木のピッチ
- -
- {/* 서까래 방향 */} -
-
垂木の方向
- -
- {/* 노지판 종류 */} -
-
路地板の種類
- -
- {/* 노지판 두께 */} -
-
路地板厚
-
- - mm -
-
- {/* 누수 흔적 */} -
-
水漏れの痕跡
- -
- {/* 방수재 종류 */} -
-
防水材の種類
- -
- {/* 단열재 유무 */} -
-
断熱材の有無
- -
- {/* 구조 순서 */} -
-
屋根構造の順序
- -
- {/* 설치 가능 여부 */} -
-
設置可能な場合
- -
- {/* 메모 */} -
-
メモ
-
- +
+
+
+
+
+
+ ) +} + +const SelectedBox = ({ + mode, + column, + detailInfoData, + setRoofInfo, +}: { + mode: Mode + column: string + detailInfoData: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { + const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] + const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] + + const [isEtcSelected, setIsEtcSelected] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') + const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + + const handleSelectChange = (e: React.ChangeEvent) => { + const value = e.target.value + const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' + const isEtc = value === 'etc' + const isSpecialEtc = isSpecialCase && value === '2' + + const updatedData: typeof detailInfoData = { + ...detailInfoData, + [column]: isEtc ? null : value, + [`${column}Etc`]: isEtc ? '' : null, + } + + if (isSpecialEtc) { + updatedData[column] = value + } + + setIsEtcSelected(isEtc || isSpecialEtc) + if (!isEtc) setEtcVal('') + setRoofInfo(updatedData) + } + + const handleEtcInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setEtcVal(value) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value }) + } + + return ( + <> + +
+ +
+ + ) +} + +const RadioSelected = ({ + mode, + column, + detailInfoData, + setRoofInfo, +}: { + mode: Mode + column: string + detailInfoData: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { + let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] + if (column === 'leakTrace') { + selectedId = Number(selectedId) + if (!selectedId) selectedId = 2 + } + + let etcValue = null + if (column !== 'rafterDirection') { + etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] + } + const [etcChecked, setEtcChecked] = useState(etcValue !== null && etcValue !== undefined && etcValue !== '') + const [etcVal, setEtcVal] = useState(etcValue?.toString() ?? '') + + const handleRadioChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (column === 'leakTrace') { + handleBooleanRadioChange(value) + } + if (value === 'etc') { + setEtcChecked(true) + setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' }) + } else { + if (column === 'insulationPresence' && value === '2') { + setEtcChecked(true) + } else { + setEtcChecked(false) + } + setRoofInfo({ ...detailInfoData, [column]: value, [`${column}Etc`]: null }) + } + } + + const handleBooleanRadioChange = (value: string) => { + if (value === '1') { + setRoofInfo({ ...detailInfoData, leakTrace: true }) + } else { + setRoofInfo({ ...detailInfoData, leakTrace: false }) + } + } + + const handleEtcInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setEtcVal(value) + setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value }) + } + + return ( + <> + {radioEtcData[column as keyof typeof radioEtcData].map((item) => ( +
+ + +
+ ))} + {column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && ( +
+ + +
+ )} + {column !== 'leakTrace' && column !== 'rafterDirection' && ( +
+ +
+ )} + + ) +} + +const MultiCheck = ({ + mode, + column, + roofInfo, + setRoofInfo, +}: { + mode: Mode + column: string + roofInfo: SurveyDetailInfo + setRoofInfo: (roofInfo: SurveyDetailRequest) => void +}) => { + const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial + + const [isOtherCheck, setIsOtherCheck] = useState(false) + const [otherValue, setOtherValue] = useState(roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? '') + + const handleCheckbox = (id: number) => { + const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) + const isOtherSelected = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null + + let newValue: string[] + if (value.includes(String(id))) { + newValue = value.filter((v) => v !== String(id)) + } else { + if (column === 'roofMaterial') { + const totalSelected = value.length + (isOtherSelected ? 1 : 0) + + if (totalSelected >= 2) { + alert('屋根材は最大2個まで選択できます。') + return + } + } + newValue = [...value, String(id)] + } + setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) + } + + const handleOtherCheckbox = () => { + if (column === 'roofMaterial') { + const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) + const currentSelected = value.length + if (!isOtherCheck && currentSelected >= 2) { + alert('屋根材は最大2個まで選択できます。') + return + } + } + const newIsOtherCheck = !isOtherCheck + setIsOtherCheck(newIsOtherCheck) + setOtherValue('') + + setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null }) + } + + const handleOtherInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + setOtherValue(value) + setRoofInfo({ ...roofInfo, [`${column}Etc`]: value }) + } + + return ( + <> +
+ {multiCheckData.map((item) => ( +
+ handleCheckbox(item.id)} + /> + +
+ ))} +
+ + +
+
+
+ +
+ + ) +} diff --git a/src/components/survey-sale/detail/form/BasicForm.tsx b/src/components/survey-sale/detail/form/BasicForm.tsx deleted file mode 100644 index 1f55838..0000000 --- a/src/components/survey-sale/detail/form/BasicForm.tsx +++ /dev/null @@ -1,244 +0,0 @@ -'use client' - -import { useServey } from '@/hooks/useSurvey' -import { SurveyBasicRequest } from '@/types/Survey' -import { useRouter, useSearchParams } from 'next/navigation' -import { useState, useEffect } from 'react' -import { useSurveySaleTabState } from '@/store/surveySaleTabState' -import { usePopupController } from '@/store/popupController' -import { useAddressStore } from '@/store/addressStore' -import { useSessionStore } from '@/store/session' -// import { useUserType } from '@/hooks/useUserType' - -const defaultBasicInfoForm: SurveyBasicRequest = { - REPRESENTATIVE: '', - STORE: null, - CONSTRUCTION_POINT: null, - INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), - BUILDING_NAME: null, - CUSTOMER_NAME: null, - POST_CODE: null, - ADDRESS: null, - ADDRESS_DETAIL: null, - SUBMISSION_STATUS: false, - SUBMISSION_DATE: null, -} - -const REQUIRED_FIELDS: (keyof SurveyBasicRequest)[] = ['REPRESENTATIVE', 'BUILDING_NAME', 'CUSTOMER_NAME'] - -export default function BasicForm() { - const searchParams = useSearchParams() - const id = searchParams.get('id') - const router = useRouter() - - const { setBasicInfoSelected } = useSurveySaleTabState() - const { surveyDetail, createSurvey, isCreatingSurvey, updateSurvey, isUpdatingSurvey } = useServey(Number(id)) - - const [basicInfoData, setBasicInfoData] = useState(defaultBasicInfoForm) - - const { addressData } = useAddressStore() - const { session } = useSessionStore() - - const popupController = usePopupController() - - useEffect(() => { - if (surveyDetail) { - const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = surveyDetail - setBasicInfoData(rest) - } - if (addressData) { - setBasicInfoData({ - ...basicInfoData, - POST_CODE: addressData.post_code, - ADDRESS: addressData.address, - ADDRESS_DETAIL: addressData.address_detail, - }) - } - if (session?.isLoggedIn) { - setBasicInfoData((prev) => ({ - ...prev, - REPRESENTATIVE: session?.userId ?? '', - STORE: session?.storeNm ?? '', - CONSTRUCTION_POINT: session?.builderNo ?? '', - })) - } - setBasicInfoSelected() - }, [surveyDetail, addressData, session?.isLoggedIn, session?.userId, session?.storeNm, session?.builderNo]) - - const focusInput = (input: keyof SurveyBasicRequest) => { - const inputElement = document.getElementById(input) - if (inputElement) { - inputElement.focus() - } - } - - const validateSurvey = (basicInfoData: SurveyBasicRequest) => { - const emptyField = REQUIRED_FIELDS.find((field) => !basicInfoData[field]) - if (emptyField) { - focusInput(emptyField) - return false - } - return true - } - - const handleChange = (key: keyof SurveyBasicRequest, value: string) => { - setBasicInfoData({ ...basicInfoData, [key]: value }) - } - - const handleSave = async (isTemporary: boolean) => { - if (id) { - // updateSurvey(basicInfoData) - alert('保存しました。') - // router.push(`/survey-sale/${id}?tab=basic-info`) - } - if (isTemporary) { - // const saveId = await createSurvey(basicInfoData) - alert('一時保存されました。') - // router.push(`/survey-sale/${saveId}?tab=basic-info`) - } else { - if (validateSurvey(basicInfoData)) { - // const saveId = await createSurvey(basicInfoData) - alert('保存しました。') - // router.push(`/survey-sale/${saveId}?tab=basic-info`) - } - } - } - - if (isCreatingSurvey || isUpdatingSurvey) { - return
Loading...
- } - - return ( - <> -
-
-
-
担当者名
- handleChange('REPRESENTATIVE', e.target.value)} - /> -
- {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( - <> -
-
販売店
- handleChange('STORE', e.target.value)} - /> -
- - )} - {(session?.role === 'Partner' || session?.role === 'Builder') && ( -
-
施工店
- handleChange('CONSTRUCTION_POINT', e.target.value)} - /> -
- )} -
-
- -
-
-
-
現地調査日
-
- - handleChange('INVESTIGATION_DATE', e.target.value)} - /> -
-
-
-
建物名
- handleChange('BUILDING_NAME', e.target.value)} - /> -
-
-
顧客名
- handleChange('CUSTOMER_NAME', e.target.value)} - /> -
-
-
建物の住所
-
-
- -
-
- -
-
-
- -
-
-
-
市区町村名, 以後の住所
- handleChange('ADDRESS_DETAIL', e.target.value)} - /> -
-
-
-
- -
-
- -
-
- -
-
-
- - ) -} diff --git a/src/components/survey-sale/detail/form/RoofInfoForm.tsx b/src/components/survey-sale/detail/form/RoofInfoForm.tsx deleted file mode 100644 index 2a465d4..0000000 --- a/src/components/survey-sale/detail/form/RoofInfoForm.tsx +++ /dev/null @@ -1,353 +0,0 @@ -'use client' - -import { useSurveySaleTabState } from '@/store/surveySaleTabState' - -import { useServey } from '@/hooks/useSurvey' -import { SurveyDetailRequest } from '@/types/Survey' -import { useRouter, useSearchParams } from 'next/navigation' -import { useEffect, useState } from 'react' -import MultiCheckEtc from './etcProcess/MultiCheckEtc' -import SelectBoxEtc from './etcProcess/SelectBoxEtc' -import RadioEtc from './etcProcess/RadioEtc' - -const defaultDetailInfoForm: SurveyDetailRequest = { - CONTRACT_CAPACITY: null, - RETAIL_COMPANY: null, - SUPPLEMENTARY_FACILITIES: null, - SUPPLEMENTARY_FACILITIES_ETC: null, - INSTALLATION_SYSTEM: null, - INSTALLATION_SYSTEM_ETC: null, - CONSTRUCTION_YEAR: null, - CONSTRUCTION_YEAR_ETC: null, - ROOF_MATERIAL: null, - ROOF_MATERIAL_ETC: null, - ROOF_SHAPE: null, - ROOF_SHAPE_ETC: null, - ROOF_SLOPE: null, - HOUSE_STRUCTURE: '1', - HOUSE_STRUCTURE_ETC: null, - RAFTER_MATERIAL: '1', - RAFTER_MATERIAL_ETC: null, - RAFTER_SIZE: null, - RAFTER_SIZE_ETC: null, - RAFTER_PITCH: null, - RAFTER_PITCH_ETC: null, - RAFTER_DIRECTION: '1', - OPEN_FIELD_PLATE_KIND: null, - OPEN_FIELD_PLATE_KIND_ETC: null, - OPEN_FIELD_PLATE_THICKNESS: null, - LEAK_TRACE: false, - WATERPROOF_MATERIAL: null, - WATERPROOF_MATERIAL_ETC: null, - INSULATION_PRESENCE: '1', - INSULATION_PRESENCE_ETC: null, - STRUCTURE_ORDER: null, - STRUCTURE_ORDER_ETC: null, - INSTALLATION_AVAILABILITY: null, - INSTALLATION_AVAILABILITY_ETC: null, - MEMO: null, -} - -export default function RoofInfoForm() { - const { setRoofInfoSelected } = useSurveySaleTabState() - - useEffect(() => { - setRoofInfoSelected() - }, []) - - const router = useRouter() - const searchParams = useSearchParams() - const id = searchParams.get('id') - - const { surveyDetail, createSurveyDetail, validateSurveyDetail } = useServey(Number(id)) - - const [detailInfoData, setDetailInfoData] = useState(defaultDetailInfoForm) - - useEffect(() => { - if (surveyDetail?.DETAIL_INFO) { - const { ID, UPT_DT, REG_DT, BASIC_INFO_ID, ...rest } = surveyDetail.DETAIL_INFO - setDetailInfoData(rest) - } - }, [surveyDetail]) - - const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { - if (key === 'ROOF_SLOPE' || key === 'OPEN_FIELD_PLATE_THICKNESS') { - const stringValue = value.toString() - if (stringValue.length > 5) { - alert('保存できるサイズを超えました。') - return - } - if (stringValue.includes('.')) { - const decimalPlaces = stringValue.split('.')[1].length - if (decimalPlaces > 1) { - alert('小数点以下1桁までしか許されません。') - return - } - } - setDetailInfoData({ ...detailInfoData, [key]: value.toString() }) - } else { - setDetailInfoData({ ...detailInfoData, [key]: value.toString() }) - } - } - - const handleTextInput = (key: keyof SurveyDetailRequest, value: string) => { - setDetailInfoData({ ...detailInfoData, [key]: value || null }) - } - - const handleBooleanInput = (key: keyof SurveyDetailRequest, value: boolean) => { - setDetailInfoData({ ...detailInfoData, [key]: value }) - } - - const handleUnitInput = (value: string) => { - const numericValue = detailInfoData.CONTRACT_CAPACITY?.replace(/[^0-9.]/g, '') || '' - setDetailInfoData({ - ...detailInfoData, - CONTRACT_CAPACITY: numericValue ? `${numericValue} ${value}` : value, - }) - } - - const handleSave = async () => { - console.log(detailInfoData) - if (id) { - const emptyField = validateSurveyDetail(detailInfoData) - if (emptyField.trim() === '') { - const updatedBasicInfoData = { - DETAIL_INFO: detailInfoData, - } - try { - createSurveyDetail({ - surveyId: Number(id), - surveyDetail: updatedBasicInfoData, - }) - alert('調査物件を保存しました。') - } catch (error) { - alert(error) - throw new Error('failed to create survey detail: ' + error) - } - router.push(`/survey-sale`) - } else { - alert(emptyField + ' は必須項目です。') - focusOnInput(emptyField) - } - } else { - alert('基本情報を作成した後、屋根情報を作成することができます。') - } - } - const focusOnInput = (field: string) => { - const input = document.getElementById(field) - if (input) { - input.focus() - } - } - return ( - <> -
-
電気関係
-
-
- {/* 전기계약 용량 - contract_capacity */} -
電気契約容量
-
- handleNumberInput('CONTRACT_CAPACITY', e.target.value)} - /> -
-
- -
-
- {/* 전기 소매 회사 - retail_company */} -
-
電気小売会社
- handleTextInput('RETAIL_COMPANY', e.target.value)} - /> -
- {/* 전기 부대 설비 - supplementary_facilities */} -
- -
- {/* 설치 희망 시스템 - installation_system */} - -
-
- -
-
屋根関係
-
- {/* 건축 연수 - construction_year */} - - {/* 지붕재 - roof_material */} -
- -
- {/* 지붕 모양 - roof_shape */} - - {/* 지붕 경사도 - roof_slope */} -
-
屋根の斜面
-
- handleNumberInput('ROOF_SLOPE', e.target.value)} - /> - -
-
- {/* 주택 구조 - house_structure */} - - {/* 서까래 재질 - rafter_material */} - - {/* 서까래 크기 - rafter_size */} - - {/* 서까래 피치 - rafter_pitch */} - - {/* 서까래 방향 - rafter_direction */} -
-
垂木の方向
-
-
- handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={detailInfoData.RAFTER_DIRECTION === '1'} - /> - -
-
- handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={detailInfoData.RAFTER_DIRECTION === '2'} - /> - -
-
-
- {/* 노지판 종류 - open_field_plate_kind */} - - {/* 노지판 두께 - open_field_plate_thickness */} -
-
- 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載 -
-
- handleNumberInput('OPEN_FIELD_PLATE_THICKNESS', e.target.value)} - /> - mm -
-
- {/* 누수 흔적 - leak_trace */} -
-
水漏れの痕跡
-
-
- handleBooleanInput('LEAK_TRACE', true)} - /> - -
-
- handleBooleanInput('LEAK_TRACE', false)} - /> - -
-
-
- {/* 방수재 종류 - waterproof_material */} - - {/* 단열재 유무 - insulation_presence */} - - {/* 노지판 종류 - open_field_plate_kind */} - - {/* 설치 가능 여부 - installation_availability */} - - {/* 메모 - memo */} -
-
メモ
-
- -
-
- -
-
-
-
-
- -
-
- -
-
- -
-
-
- - ) -} diff --git a/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx deleted file mode 100644 index 30a1ca7..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/MultiCheckEtc.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { SurveyDetailRequest } from '@/types/Survey' -import { useEffect, useState } from 'react' - -export const supplementary_facilities = [ - { id: 1, name: 'エコキュート' }, //에코큐트 - { id: 2, name: 'エネパーム' }, //에네팜 - { id: 3, name: '蓄電池システム' }, //축전지시스템 - { id: 4, name: '太陽光発電' }, //태양광발전 -] - -export const roof_material = [ - { id: 1, name: 'スレート' }, //슬레이트 - { id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글 - { id: 3, name: '瓦' }, //기와 - { id: 4, name: '金属屋根' }, //금속지붕 -] - -export default function MultiCheckbox({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: string - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const selectList = column === 'SUPPLEMENTARY_FACILITIES' ? supplementary_facilities : roof_material - - const [isOtherChecked, setIsOtherChecked] = useState(false) - const [otherValue, setOtherValue] = useState('') - - const makeNumArr = (value: string) => { - return value - .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0) - } - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsOtherChecked(true) - setOtherValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleCheckbox = (dataIndex: number) => { - const value = makeNumArr(String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')) - - let newValue: string[] - if (value.includes(String(dataIndex))) { - // 체크 해제 - newValue = value.filter((v) => v !== String(dataIndex)) - } else { - // 체크 - if (column === 'ROOF_MATERIAL') { - // 기타가 체크되어 있는지 확인 - const isOtherSelected = isOtherChecked - // 현재 선택된 항목 수 + 기타 선택 여부 - const totalSelected = value.length + (isOtherSelected ? 1 : 0) - - if (totalSelected >= 2) { - alert('屋根材は最大2個まで選択可能です。') - return - } - } - newValue = [...value, String(dataIndex)] - } - - setDetailInfoData({ - ...detailInfoData, - [column]: newValue.join(', '), - }) - } - - const handleOtherCheckbox = () => { - if (column === 'ROOF_MATERIAL') { - const value = makeNumArr(String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')) - const currentSelected = value.length - if (!isOtherChecked && currentSelected >= 2) { - alert('Up to two roofing materials can be selected.') - return - } - } - - const newIsOtherChecked = !isOtherChecked - setIsOtherChecked(newIsOtherChecked) - setOtherValue('') - - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: newIsOtherChecked ? '' : null, - }) - } - - const handleOtherInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setOtherValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( - <> - {column === 'SUPPLEMENTARY_FACILITIES' ? ( - <> -
- 電気袋設備※複数選択可能 -
- - ) : ( - <> -
- 屋根材※最大2個まで選択可能 -
- - )} -
- {selectList.map((item) => ( -
- handleCheckbox(item.id)} - /> - -
- ))} -
- - -
-
-
- -
- - ) -} diff --git a/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx deleted file mode 100644 index 29f3325..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/RadioEtc.tsx +++ /dev/null @@ -1,175 +0,0 @@ -'use client' -import { useEffect, useState } from 'react' -import { SurveyDetailRequest } from '@/types/Survey' - -type RadioEtcKeys = 'HOUSE_STRUCTURE' | 'RAFTER_MATERIAL' | 'WATERPROOF_MATERIAL' | 'INSULATION_PRESENCE' | 'RAFTER_DIRECTION' | 'LEAK_TRACE' - -const translateJapanese: Record = { - HOUSE_STRUCTURE: '住宅構造', - RAFTER_MATERIAL: '垂木材質', - WATERPROOF_MATERIAL: '防水材の種類', - INSULATION_PRESENCE: '断熱材の有無', - RAFTER_DIRECTION: '垂木の方向', - LEAK_TRACE: '水漏れの痕跡', -} - -export const radioEtcData: Record = { - HOUSE_STRUCTURE: [ - { - id: 1, - label: '木製', - }, - ], - RAFTER_MATERIAL: [ - { - id: 1, - label: '木製', - }, - { - id: 2, - label: '強制', - }, - ], - WATERPROOF_MATERIAL: [ - { - id: 1, - label: 'アスファルト屋根940(22kg以上)', - }, - ], - INSULATION_PRESENCE: [ - { - id: 1, - label: 'なし', - }, - { - id: 2, - label: 'あり', - }, - ], - RAFTER_DIRECTION: [ - { - id: 1, - label: '垂直垂木', - }, - { - id: 2, - label: '水平垂木', - }, - ], - LEAK_TRACE: [ - { - id: 1, - label: 'あり', - }, - { - id: 2, - label: 'なし', - }, - ], -} - -export default function RadioEtc({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: RadioEtcKeys - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const [isEtcSelected, setIsEtcSelected] = useState(false) - const [etcValue, setEtcValue] = useState('') - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsEtcSelected(true) - setEtcValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleRadioChange = (e: React.ChangeEvent) => { - // const value = e.target.value - // if (column === 'INSULATION_PRESENCE') { - // setIsEtcSelected(value === '2') - // setDetailInfoData({ - // ...detailInfoData, - // [column]: value, - // }) - // } else if (value === 'etc') { - // setIsEtcSelected(true) - // setDetailInfoData({ - // ...detailInfoData, - // [column]: null, - // }) - // } else { - // setIsEtcSelected(false) - // setEtcValue('') - // setDetailInfoData({ - // ...detailInfoData, - // [column]: value, - // [`${column}_ETC`]: null, - // }) - // } - const value = e.target.value - const isSpecialCase = column === 'INSULATION_PRESENCE' - const isEtc = value === 'etc' - const isSpecialEtc = isSpecialCase && value === '2' - - const updatedData: typeof detailInfoData = { - ...detailInfoData, - [column]: isEtc ? null : value, - [`${column}_ETC`]: isEtc ? '' : null, - } - - if (isSpecialEtc) { - updatedData[column] = value - } - - setIsEtcSelected(isEtc || isSpecialEtc) - if (!isEtc) setEtcValue('') - setDetailInfoData(updatedData) - } - - const handleEtcInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setEtcValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( -
-
{translateJapanese[column]}
- {radioEtcData[column].map((item) => ( -
- - -
- ))} - {column !== 'INSULATION_PRESENCE' && ( -
- - -
- )} -
- -
-
- ) -} diff --git a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx b/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx deleted file mode 100644 index 9469a40..0000000 --- a/src/components/survey-sale/detail/form/etcProcess/SelectBoxEtc.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import type { SurveyDetailRequest } from '@/types/Survey' -import { useEffect, useState } from 'react' - -export type SelectBoxKeys = - | 'INSTALLATION_SYSTEM' - | 'CONSTRUCTION_YEAR' - | 'ROOF_SHAPE' - | 'RAFTER_PITCH' - | 'RAFTER_SIZE' - | 'OPEN_FIELD_PLATE_KIND' - | 'STRUCTURE_ORDER' - | 'INSTALLATION_AVAILABILITY' - -const font: Record = { - INSTALLATION_SYSTEM: 'data-input-form-tit red-f', - CONSTRUCTION_YEAR: 'data-input-form-tit red-f', - ROOF_SHAPE: 'data-input-form-tit', - RAFTER_PITCH: 'data-input-form-tit red-f', - RAFTER_SIZE: 'data-input-form-tit red-f', - OPEN_FIELD_PLATE_KIND: 'data-input-form-tit', - STRUCTURE_ORDER: 'data-input-form-tit red-f', - INSTALLATION_AVAILABILITY: 'data-input-form-tit', -} - -const translateJapanese: Record = { - INSTALLATION_SYSTEM: '設置希望システム', - CONSTRUCTION_YEAR: '建築年数', - ROOF_SHAPE: '屋根の形状', - RAFTER_PITCH: '垂木傾斜', - RAFTER_SIZE: '垂木サイズ', - OPEN_FIELD_PLATE_KIND: '路地板の種類', - STRUCTURE_ORDER: '屋根構造の順序', - INSTALLATION_AVAILABILITY: '屋根製品名 設置可否確認', -} - -export const selectBoxOptions: Record = { - INSTALLATION_SYSTEM: [ - { - id: 1, - name: '太陽光発電', //태양광발전 - }, - { - id: 2, - name: 'ハイブリッド蓄電システム', //하이브리드축전지시스템 - }, - { - id: 3, - name: '蓄電池システム', //축전지시스템 - }, - ], - CONSTRUCTION_YEAR: [ - { - id: 1, - name: '新築', //신축 - }, - { - id: 2, - name: '既築', //기존 - }, - ], - ROOF_SHAPE: [ - { - id: 1, - name: '切妻', //박공지붕 - }, - { - id: 2, - name: '寄棟', //기동 - }, - { - id: 3, - name: '片流れ', //한쪽흐름 - }, - ], - RAFTER_SIZE: [ - { - id: 1, - name: '幅35mm以上×高さ48mm以上', - }, - { - id: 2, - name: '幅36mm以上×高さ46mm以上', - }, - { - id: 3, - name: '幅37mm以上×高さ43mm以上', - }, - { - id: 4, - name: '幅38mm以上×高さ40mm以上', - }, - ], - RAFTER_PITCH: [ - { - id: 1, - name: '(455mm以下', - }, - { - id: 2, - name: '500mm以下', - }, - { - id: 3, - name: '606mm以下', - }, - ], - OPEN_FIELD_PLATE_KIND: [ - { - id: 1, - name: '構造用合板', //구조용합판 - }, - { - id: 2, - name: 'OSB', //OSB - }, - { - id: 3, - name: 'パーティクルボード', //파티클보드 - }, - { - id: 4, - name: '小幅板', //소판 - }, - ], - STRUCTURE_ORDER: [ - { - id: 1, - name: '屋根材', //지붕재 - }, - { - id: 2, - name: '防水材', //방수재 - }, - { - id: 3, - name: '屋根の基礎', //지붕의기초 - }, - { - id: 4, - name: '垂木', //서까래 - }, - ], - INSTALLATION_AVAILABILITY: [ - { - id: 1, - name: '確認済み', //확인완료 - }, - { - id: 2, - name: '未確認', //미확인 - }, - ], -} - -export default function SelectBoxForm({ - column, - setDetailInfoData, - detailInfoData, -}: { - column: SelectBoxKeys - setDetailInfoData: (data: any) => void - detailInfoData: SurveyDetailRequest -}) { - const [isEtcSelected, setIsEtcSelected] = useState(false) - const [etcValue, setEtcValue] = useState('') - - useEffect(() => { - if (detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest]) { - setIsEtcSelected(true) - setEtcValue(detailInfoData[`${column}_ETC` as keyof SurveyDetailRequest] as string) - } - }, [detailInfoData]) - - const handleSelectChange = (e: React.ChangeEvent) => { - const value = e.target.value - const isSpecialCase = column === 'CONSTRUCTION_YEAR' || column === 'INSTALLATION_AVAILABILITY' - const isEtc = value === 'etc' - const isSpecialEtc = isSpecialCase && value === '2' - - const updatedData: typeof detailInfoData = { - ...detailInfoData, - [column]: isEtc ? null : value, - [`${column}_ETC`]: isEtc ? '' : null, - } - - // 건축연수 + 설치가능여부는 2번 선택 시 input 활성화 - if (isSpecialEtc) { - updatedData[column] = value - } - - setIsEtcSelected(isEtc || isSpecialEtc) - if (!isEtc) setEtcValue('') - setDetailInfoData(updatedData) - } - - const handleEtcInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setEtcValue(value) - setDetailInfoData({ - ...detailInfoData, - [`${column}_ETC`]: value, - }) - } - - return ( - <> -
-
{translateJapanese[column as keyof typeof translateJapanese]}
-
- -
-
- -
-
- - ) -} diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index 16351b7..f1a3847 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -7,34 +7,37 @@ import { useRouter } from 'next/navigation' import SearchForm from './SearchForm' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSessionStore } from '@/store/session' +import type { SurveyBasicInfo } from '@/types/Survey' export default function ListTable() { const router = useRouter() - const { surveyList, isLoadingSurveyList, surveyListCount } = useServey() + const { surveyList, isLoadingSurveyList } = useServey() const { offset, setOffset } = useSurveyFilterStore() - const [heldSurveyList, setHeldSurveyList] = useState([]) + const [heldSurveyList, setHeldSurveyList] = useState([]) const [hasMore, setHasMore] = useState(false) const { session } = useSessionStore() useEffect(() => { - if (surveyList && surveyList.length > 0) { - if (offset === 0) { - setHeldSurveyList(surveyList) + if (!session.isLoggedIn || !('data' in surveyList)) return + if ('count' in surveyList && surveyList.count > 0) { + if (offset > 0) { + setHeldSurveyList((prev) => [...prev, ...surveyList.data]) } else { - const remainingList = heldSurveyList.slice(offset, offset + 10) - if (JSON.stringify(remainingList) !== JSON.stringify(surveyList)) { - setHeldSurveyList((prev) => [...prev, ...surveyList]) - } + setHeldSurveyList(surveyList.data) } - setHasMore(surveyListCount > offset + 10) + setHasMore(surveyList.count > offset + 10) + } else { + setHeldSurveyList([]) + setHasMore(false) } - }, [surveyList, surveyListCount, offset, session?.role]) + }, [surveyList, offset, session]) const handleDetailClick = (id: number) => { router.push(`/survey-sale/${id}`) } + const handleItemsInit = () => { setHeldSurveyList([]) setOffset(0) @@ -44,22 +47,22 @@ export default function ListTable() { return ( <> - + {heldSurveyList.length > 0 ? (
    {heldSurveyList.map((survey) => ( -
  • handleDetailClick(survey.ID)}> +
  • handleDetailClick(survey.id)}>
    -
    {survey.ID}
    -
    {survey.INVESTIGATION_DATE}
    +
    {survey.id}
    +
    {survey.investigationDate}
    -
    {survey.BUILDING_NAME}
    -
    {survey.CUSTOMER_NAME}
    +
    {survey.buildingName}
    +
    {survey.customerName}
    -
    {survey.REPRESENTATIVE}
    -
    {new Date(survey.UPT_DT).toLocaleString()}
    +
    {survey.representative}
    +
    {new Date(survey.uptDt).toLocaleString()}
  • diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 3f3d234..7f46e68 100644 --- a/src/components/survey-sale/list/SearchForm.tsx +++ b/src/components/survey-sale/list/SearchForm.tsx @@ -4,7 +4,7 @@ import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS, useSurvey import { useRouter } from 'next/navigation' import { useState } from 'react' -export default function SearchForm({ onItemsInit, memberRole, userId }: { onItemsInit: () => void; memberRole: string; userId: string }) { +export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) { const router = useRouter() const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore() const [searchKeyword, setSearchKeyword] = useState(keyword) @@ -17,7 +17,6 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem } setKeyword(searchKeyword) setSearchOption(option) - onItemsInit() } const searchOptions = memberRole === 'Partner' ? SEARCH_OPTIONS_PARTNERS : SEARCH_OPTIONS @@ -38,7 +37,6 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem if (e.target.value === 'all') { setKeyword('') setSearchKeyword('') - onItemsInit() setSearchOption('all') setOption('all') } else { @@ -80,7 +78,6 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem checked={isMySurvey === userId} onChange={() => { setIsMySurvey(isMySurvey === userId ? null : userId) - onItemsInit() }} /> @@ -94,7 +91,6 @@ export default function SearchForm({ onItemsInit, memberRole, userId }: { onItem value={sort} onChange={(e) => { setSort(e.target.value as 'created' | 'updated') - onItemsInit() }} > diff --git a/src/components/survey-sale/temp/basicRegist.tsx b/src/components/survey-sale/temp/basicRegist.tsx deleted file mode 100644 index 68d59ba..0000000 --- a/src/components/survey-sale/temp/basicRegist.tsx +++ /dev/null @@ -1,156 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyRegistRequest } from '@/types/Survey' -import { useEffect } from 'react' -import { usePopupController } from '@/store/popupController' -import { useAddressStore } from '@/store/addressStore' -import { useSessionStore } from '@/store/session' - - -export default function BasicRegist({ - basicInfoData, - setBasicInfoData, -}: { - basicInfoData: SurveyBasicRequest - setBasicInfoData: (data: SurveyBasicRequest) => void -}) { - - const { addressData } = useAddressStore() - const { session } = useSessionStore() - - const popupController = usePopupController() - - useEffect(() => { - if (addressData) { - setBasicInfoData({ - ...basicInfoData, - POST_CODE: addressData.post_code, - ADDRESS: addressData.address, - ADDRESS_DETAIL: addressData.address_detail, - }) - } - }, [addressData]) - - const handleChange = (key: keyof SurveyRegistRequest, value: string) => { - setBasicInfoData({ ...basicInfoData, [key]: value }) - } - - return ( - <> -
    -
    -
    -
    担当者名
    - -
    - {(session?.role === 'Builder' || session?.role?.includes('Admin')) && ( - <> -
    -
    販売店
    - -
    - - )} - {(session?.role === 'Partner' || session?.role === 'Builder') && ( -
    -
    施工店
    - -
    - )} -
    -
    - -
    -
    -
    -
    現地調査日
    -
    - - handleChange('INVESTIGATION_DATE', e.target.value)} - /> -
    -
    -
    -
    建物名
    - handleChange('BUILDING_NAME', e.target.value)} - /> -
    -
    -
    顧客名
    - handleChange('CUSTOMER_NAME', e.target.value)} - /> -
    -
    -
    建物の住所
    -
    -
    - -
    -
    - handleChange('ADDRESS', e.target.value)} - readOnly - /> -
    -
    -
    - -
    -
    -
    -
    市区町村名, 以後の住所
    - handleChange('ADDRESS_DETAIL', e.target.value)} - /> -
    -
    -
    - - ) -} diff --git a/src/components/survey-sale/temp/formButton.tsx b/src/components/survey-sale/temp/formButton.tsx deleted file mode 100644 index 7b41003..0000000 --- a/src/components/survey-sale/temp/formButton.tsx +++ /dev/null @@ -1,92 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyRegistRequest } from '@/types/Survey' -import { SurveyDetailRequest } from '@/types/Survey' -import { useRouter } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' - -export default function FormButton({ - surveyData, - idParam, -}: { - surveyData: { basic: SurveyBasicRequest; roof: SurveyDetailRequest } - idParam: string | null -}) { - const router = useRouter() - const { validateSurveyDetail, createSurvey, updateSurvey } = useServey(Number(idParam)) - - const saveData = { - ...surveyData.basic, - DETAIL_INFO: surveyData.roof, - } - - const focusInput = (input: keyof SurveyRegistRequest) => { - const inputElement = document.getElementById(input) - if (inputElement) { - inputElement.focus() - } - } - - const handleSave = (isTemporary: boolean) => { - const emptyField = validateSurveyDetail(saveData.DETAIL_INFO) - if (!isTemporary) { - saveProcess(emptyField) - } else { - temporarySaveProcess() - } - } - const saveProcess = async (emptyField: string) => { - if (emptyField.trim() === '') { - if (idParam) { - // 매물 수정 (저장) - updateSurvey(saveData) - router.push(`/survey-sale/${idParam}`) - } else { - // 매물 생성 (저장) - const id = await createSurvey(saveData) - router.push(`/survey-sale/${id}`) - } - alert('保存されました。') - } else { - alert(emptyField + ' 項目が空です。') - focusInput(emptyField as keyof SurveyRegistRequest) - } - } - - const temporarySaveProcess = async () => { - if (idParam) { - // 매물 수정 (임시저장) - updateSurvey(saveData) - router.push(`/survey-sale/${idParam}?isTemporary=true`) - } else { - // 매물 생성 (임시저장) - const id = await createSurvey(saveData) - router.push(`/survey-sale/${id}?isTemporary=true`) - } - alert('一時保存されました。') - } - - return ( - <> -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    - - ) -} diff --git a/src/components/survey-sale/temp/registForm.tsx b/src/components/survey-sale/temp/registForm.tsx deleted file mode 100644 index 42e7267..0000000 --- a/src/components/survey-sale/temp/registForm.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client' - -import { SurveyBasicRequest, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' -import FormButton from './formButton' -import { useEffect, useState } from 'react' -import BasicRegist from './basicRegist' -import RoofRegist from './roofRegist' -import { useSessionStore } from '@/store/session' -import { useSearchParams } from 'next/navigation' -import { useServey } from '@/hooks/useSurvey' - -const roofInfoForm: SurveyDetailRequest = { - CONTRACT_CAPACITY: null, - RETAIL_COMPANY: null, - SUPPLEMENTARY_FACILITIES: null, - SUPPLEMENTARY_FACILITIES_ETC: null, - INSTALLATION_SYSTEM: null, - INSTALLATION_SYSTEM_ETC: null, - CONSTRUCTION_YEAR: null, - CONSTRUCTION_YEAR_ETC: null, - ROOF_MATERIAL: null, - ROOF_MATERIAL_ETC: null, - ROOF_SHAPE: null, - ROOF_SHAPE_ETC: null, - ROOF_SLOPE: null, - HOUSE_STRUCTURE: '1', - HOUSE_STRUCTURE_ETC: null, - RAFTER_MATERIAL: '1', - RAFTER_MATERIAL_ETC: null, - RAFTER_SIZE: null, - RAFTER_SIZE_ETC: null, - RAFTER_PITCH: null, - RAFTER_PITCH_ETC: null, - RAFTER_DIRECTION: '1', - OPEN_FIELD_PLATE_KIND: null, - OPEN_FIELD_PLATE_KIND_ETC: null, - OPEN_FIELD_PLATE_THICKNESS: null, - LEAK_TRACE: false, - WATERPROOF_MATERIAL: null, - WATERPROOF_MATERIAL_ETC: null, - INSULATION_PRESENCE: '1', - INSULATION_PRESENCE_ETC: null, - STRUCTURE_ORDER: null, - STRUCTURE_ORDER_ETC: null, - INSTALLATION_AVAILABILITY: null, - INSTALLATION_AVAILABILITY_ETC: null, - MEMO: null, -} - -const basicInfoForm: SurveyBasicRequest = { - REPRESENTATIVE: '', - STORE: null, - CONSTRUCTION_POINT: null, - INVESTIGATION_DATE: new Date().toLocaleDateString('en-CA'), - BUILDING_NAME: null, - CUSTOMER_NAME: null, - POST_CODE: null, - ADDRESS: null, - ADDRESS_DETAIL: null, - SUBMISSION_STATUS: false, - SUBMISSION_DATE: null, -} - -export default function RegistForm() { - const searchParams = useSearchParams() - const id = searchParams.get('id') - - const { session } = useSessionStore() - const { surveyDetail } = useServey(Number(id)) - - const [basicInfoData, setBasicInfoData] = useState(basicInfoForm) - const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) - - useEffect(() => { - if (session) { - setBasicInfoData({ - ...basicInfoForm, - REPRESENTATIVE: session.userNm ?? '', - STORE: session.role === 'T01' ? '' : session.storeNm ?? '', - CONSTRUCTION_POINT: session.builderNo ?? '', - }) - } - if (id && surveyDetail) { - const { ID, UPT_DT, REG_DT, DETAIL_INFO, ...rest } = surveyDetail - setBasicInfoData(rest) - if (surveyDetail?.DETAIL_INFO) { - const { ID, UPT_DT, REG_DT, BASIC_INFO_ID, ...rest } = surveyDetail.DETAIL_INFO - setRoofInfoData(rest) - } - } - }, [session, surveyDetail]) - - const surveyData = { - basic: basicInfoData, - roof: roofInfoData, - } - - return ( - <> - - - - - ) -} diff --git a/src/components/survey-sale/temp/roofRegist.tsx b/src/components/survey-sale/temp/roofRegist.tsx deleted file mode 100644 index 6c5492a..0000000 --- a/src/components/survey-sale/temp/roofRegist.tsx +++ /dev/null @@ -1,284 +0,0 @@ -'use client' - -import { useSurveySaleTabState } from '@/store/surveySaleTabState' - -import { SurveyBasicInfo, SurveyDetailRequest } from '@/types/Survey' -import { useEffect } from 'react' -import MultiCheckEtc from '../detail/form/etcProcess/MultiCheckEtc' -import SelectBoxEtc from '../detail/form/etcProcess/SelectBoxEtc' -import RadioEtc from '../detail/form/etcProcess/RadioEtc' - -export default function RoofRegist({ - roofInfoData, - setRoofInfoData, -}: { - roofInfoData: SurveyDetailRequest - setRoofInfoData: (data: SurveyDetailRequest) => void -}) { - const { setRoofInfoSelected } = useSurveySaleTabState() - - useEffect(() => { - setRoofInfoSelected() - }, []) - - const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { - if (key === 'ROOF_SLOPE' || key === 'OPEN_FIELD_PLATE_THICKNESS') { - const stringValue = value.toString() - if (stringValue.length > 5) { - alert('保存できるサイズを超えました。') - return - } - if (stringValue.includes('.')) { - const decimalPlaces = stringValue.split('.')[1].length - if (decimalPlaces > 1) { - alert('小数点以下1桁までしか許されません。') - return - } - } - } - setRoofInfoData({ ...roofInfoData, [key]: value.toString() }) - } - - const handleTextInput = (key: keyof SurveyDetailRequest, value: string) => { - setRoofInfoData({ ...roofInfoData, [key]: value || null }) - } - - const handleBooleanInput = (key: keyof SurveyDetailRequest, value: boolean) => { - setRoofInfoData({ ...roofInfoData, [key]: value }) - } - - const handleUnitInput = (value: string) => { - const numericValue = roofInfoData.CONTRACT_CAPACITY?.replace(/[^0-9.]/g, '') || '' - setRoofInfoData({ - ...roofInfoData, - CONTRACT_CAPACITY: numericValue ? `${numericValue} ${value}` : value, - }) - } - - // const handleSave = async () => { - // if (id) { - // const emptyField = validateSurveyDetail(roofInfoData) - // if (emptyField.trim() === '') { - // const updatedBasicInfoData = { - // DETAIL_INFO: roofInfoData, - // } - // try { - // createSurveyDetail({ - // surveyId: Number(id), - // surveyDetail: updatedBasicInfoData, - // }) - // alert('調査物件を保存しました。') - // } catch (error) { - // alert(error) - // throw new Error('failed to create survey detail: ' + error) - // } - // router.push(`/survey-sale`) - // } else { - // alert(emptyField + ' は必須項目です。') - // focusOnInput(emptyField) - // } - // } else { - // alert('基本情報を作成した後、屋根情報を作成することができます。') - // } - // } - // const focusOnInput = (field: string) => { - // const input = document.getElementById(field) - // if (input) { - // input.focus() - // } - // } - return ( - <> -
    -
    電気関係
    -
    -
    - {/* 전기계약 용량 - contract_capacity */} -
    電気契約容量
    -
    - handleNumberInput('CONTRACT_CAPACITY', e.target.value)} - /> -
    -
    - -
    -
    - {/* 전기 소매 회사 - retail_company */} -
    -
    電気小売会社
    - handleTextInput('RETAIL_COMPANY', e.target.value)} - /> -
    - {/* 전기 부대 설비 - supplementary_facilities */} -
    - -
    - {/* 설치 희망 시스템 - installation_system */} - -
    -
    - -
    -
    屋根関係
    -
    - {/* 건축 연수 - construction_year */} - - {/* 지붕재 - roof_material */} -
    - -
    - {/* 지붕 모양 - roof_shape */} - - {/* 지붕 경사도 - roof_slope */} -
    -
    屋根の斜面
    -
    - handleNumberInput('ROOF_SLOPE', e.target.value)} - /> - -
    -
    - {/* 주택 구조 - house_structure */} - - {/* 서까래 재질 - rafter_material */} - - {/* 서까래 크기 - rafter_size */} - - {/* 서까래 피치 - rafter_pitch */} - - {/* 서까래 방향 - rafter_direction */} -
    -
    垂木の方向
    -
    -
    - handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={roofInfoData.RAFTER_DIRECTION === '1'} - /> - -
    -
    - handleNumberInput('RAFTER_DIRECTION', Number(e.target.value))} - checked={roofInfoData.RAFTER_DIRECTION === '2'} - /> - -
    -
    -
    - {/* 노지판 종류 - open_field_plate_kind */} - - {/* 노지판 두께 - open_field_plate_thickness */} -
    -
    - 路地板厚※小幅板を選択した場合, 厚さ. 小幅板間の間隔寸法を記載 -
    -
    - handleNumberInput('OPEN_FIELD_PLATE_THICKNESS', e.target.value)} - /> - mm -
    -
    - {/* 누수 흔적 - leak_trace */} -
    -
    水漏れの痕跡
    -
    -
    - handleBooleanInput('LEAK_TRACE', true)} - /> - -
    -
    - handleBooleanInput('LEAK_TRACE', false)} - /> - -
    -
    -
    - {/* 방수재 종류 - waterproof_material */} - - {/* 단열재 유무 - insulation_presence */} - - {/* 노지판 종류 - open_field_plate_kind */} - - {/* 설치 가능 여부 - installation_availability */} - - {/* 메모 - memo */} -
    -
    メモ
    -
    - -
    -
    - -
    -
    -
    -
    - - ) -} diff --git a/src/components/ui/common/Footer.tsx b/src/components/ui/common/Footer.tsx index dedeb50..08cce69 100644 --- a/src/components/ui/common/Footer.tsx +++ b/src/components/ui/common/Footer.tsx @@ -1,8 +1,17 @@ +'use client' + +import Link from 'next/link' + export default function Footer() { return ( <>
    -
    COPYRIGHT©2025 Hanwha Japan All Rights Reserved
    +
    + COPYRIGHT©2025 Hanwha Japan All Rights Reserved{' '} + + PDF + +
    ) diff --git a/src/components/ui/common/Header.tsx b/src/components/ui/common/Header.tsx index da3cf49..bd92675 100644 --- a/src/components/ui/common/Header.tsx +++ b/src/components/ui/common/Header.tsx @@ -71,7 +71,7 @@ export default function Header() {
{session.userNm}
-
{session.category}
+
{session.storeNm}
@@ -114,9 +114,11 @@ export default function Header() {
  • -
  • - -
  • + {session.role !== 'Partner' && ( +
  • + +
  • + )}
    diff --git a/src/hooks/useCommCode.ts b/src/hooks/useCommCode.ts new file mode 100644 index 0000000..bb50240 --- /dev/null +++ b/src/hooks/useCommCode.ts @@ -0,0 +1,18 @@ +import { axiosInstance } from '@/libs/axios' +import type { CommCode } from '@/types/CommCode' + +export function useCommCode() { + const getCommCode = async (headCode: string): Promise => { + try { + const response = await axiosInstance(null).get('/api/comm-code', { params: { headCode: headCode } }) + return response.data + } catch (error) { + console.error(`common code (${headCode}) load failed:`, error) + return [] + } + } + + return { + getCommCode, + } +} diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index c3f0dde..4bdd9b2 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -1,30 +1,107 @@ -import { suitableApi } from '@/api/suitable' +import { useQuery } from '@tanstack/react-query' +import { axiosInstance, transformObjectKeys } from '@/libs/axios' +import { useSuitableStore } from '@/store/useSuitableStore' +import { useCommCode } from './useCommCode' +import { SUITABLE_HEAD_CODE, type SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable' export function useSuitable() { - const getCategories = async () => { + const { getCommCode } = useCommCode() + const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + + const getSuitables = async (): Promise => { try { - // return await suitableApi.getCategory() + const response = await axiosInstance(null).get('/api/suitable/list') + return response.data } catch (error) { - console.error('카테고리 데이터 로드 실패:', error) + console.error('지붕재 데이터 로드 실패:', error) + return { suitable: [], detail: [] } + } + } + + // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => { + // try { + // const response = await axiosInstance(null).get('/api/suitable/list', { params: { selectedCategory, searchValue } }) + // return response.data + // } catch (error) { + // console.error('지붕재 데이터 검색 실패:', error) + // return [] + // } + // } + + const getSuitableCommCode = () => { + const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] + for (const code of headCodes) { + getCommCode(code).then((res) => { + setSuitableCommCode(code, res) + }) + } + } + + const toCodeName = (headCode: string, code: string): string => { + const commCode = suitableCommCode.get(headCode) + return commCode?.find((item) => item.code === code)?.codeJp || '' + } + + const toSuitableDetail = (mainId: number): SuitableDetail[] => { + try { + const suitableDetailString = suitableList?.detail.find((item) => item.mainId === mainId)?.detail + if (!suitableDetailString) { + return [] + } + const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[] + if (!Array.isArray(suitableDetailArray)) { + throw new Error('suitableDetailArray is not an array') + } + return suitableDetailArray + } catch (error) { + console.error('지붕재 데이터 파싱 실패:', error) return [] } } - const getSuitables = async () => { - try { - // return await suitableApi.getList() - } catch (error) { - console.error('지붕재 데이터 로드 실패:', error) - } - } + const { data: suitableList, isLoading: isInitialLoading } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10분 + gcTime: 1000 * 60 * 10, // 10분 + }) - const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => { - try { - // return await suitableApi.getList(selectedCategory, searchValue) - } catch (error) { - console.error('지붕재 데이터 검색 실패:', error) - } - } + const { + data: suitableSearchResults, + refetch: refetchBySearch, + isLoading: isSearchLoading, + } = useQuery({ + queryKey: ['suitables', 'search', selectedCategory, isSearch], + queryFn: async () => { + if (!isSearch && !selectedCategory) { + // 검색 상태가 아니면 초기 데이터 반환 임시처리 + return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] } + } else { + const filteredSuitable = suitableList?.suitable.filter((item: SuitableMain) => { + const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory + const searchMatch = !searchValue || item.productName.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + const mainIds = filteredSuitable.map((item: SuitableMain) => item.id) + const filteredDetail = suitableList?.detail.filter((item: SuitableDetailGroup) => { + return mainIds.includes(item.mainId) + }) ?? [] + return { suitable: filteredSuitable, detail: filteredDetail } + } + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: true, + }) - return { getCategories, getSuitables, updateSearchResults } + return { + getSuitables, + getSuitableCommCode, + toCodeName, + toSuitableDetail, + suitableList, + suitableSearchResults, + refetchBySearch, + isSearchLoading, + } } diff --git a/src/hooks/useSuitableRaw.ts b/src/hooks/useSuitableRaw.ts new file mode 100644 index 0000000..a962244 --- /dev/null +++ b/src/hooks/useSuitableRaw.ts @@ -0,0 +1,109 @@ +import { useQuery } from '@tanstack/react-query' +import { axiosInstance, transformObjectKeys } from '@/libs/axios' +import { useSuitableStore } from '@/store/useSuitableStore' +import { useCommCode } from './useCommCode' +import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable' + +export type Suitable = { + id: number + productName: string + manuFtCd: string + roofMtCd: string + roofShCd: string + detail: string +} + +export function useSuitableRaw() { + const { getCommCode } = useCommCode() + const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() + + const getSuitables = async (): Promise => { + try { + const response = await axiosInstance(null).get('/api/suitable/list/test') + return response.data + } catch (error) { + console.error('지붕재 데이터 로드 실패:', error) + return [] + } + } + + // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => { + // try { + // const response = await axiosInstance(null).get('/api/suitable/list', { params: { selectedCategory, searchValue } }) + // return response.data + // } catch (error) { + // console.error('지붕재 데이터 검색 실패:', error) + // return [] + // } + // } + + const getSuitableCommCode = () => { + const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] + for (const code of headCodes) { + getCommCode(code).then((res) => { + setSuitableCommCode(code, res) + }) + } + } + + const toCodeName = (headCode: string, code: string): string => { + const commCode = suitableCommCode.get(headCode) + return commCode?.find((item) => item.code === code)?.codeJp || '' + } + + const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => { + try { + const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[] + if (!Array.isArray(suitableDetailArray)) { + throw new Error('suitableDetailArray is not an array') + } + return suitableDetailArray + } catch (error) { + console.error('지붕재 데이터 파싱 실패:', error) + return [] + } + } + + const { data: suitableList, isLoading: isInitialLoading } = useQuery({ + queryKey: ['suitables', 'list'], + queryFn: async () => await getSuitables(), + staleTime: 1000 * 60 * 10, // 10분 + gcTime: 1000 * 60 * 10, // 10분 + }) + + const { + data: suitableSearchResults, + refetch: refetchBySearch, + isLoading: isSearchLoading, + // } = useQuery({ + } = useQuery({ + queryKey: ['suitables', 'search', selectedCategory, isSearch], + queryFn: async () => { + if (!isSearch && !selectedCategory) { + return isInitialLoading ? await getSuitables() : suitableList ?? [] // 검색 상태가 아니면 초기 데이터 반환 임시처리 + } else { + return ( + suitableList?.filter((item: Suitable) => { + const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory + const searchMatch = !searchValue || item.productName.includes(searchValue) + return categoryMatch && searchMatch + }) ?? [] + ) + } + }, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 10, + enabled: true, + }) + + return { + getSuitables, + getSuitableCommCode, + toCodeName, + toSuitableDetail, + suitableList, + suitableSearchResults, + refetchBySearch, + isSearchLoading, + } +} diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index a047706..043cfce 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,43 +1,39 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import type { - SurveyBasicInfo, - SurveyDetailInfo, - SurveyDetailRequest, - SurveyDetailCoverRequest, - SurveyRegistRequest, -} from '@/types/Survey' +import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey' import { axiosInstance } from '@/libs/axios' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { queryStringFormatter } from '@/utils/common-utils' import { useSessionStore } from '@/store/session' +import { useMemo } from 'react' +import { AxiosResponse } from 'axios' -const requiredFields = [ +export const requiredFields = [ { - field: 'INSTALLATION_SYSTEM', + field: 'installationSystem', name: '設置希望システム', }, { - field: 'CONSTRUCTION_YEAR', + field: 'constructionYear', name: '建築年数', }, { - field: 'RAFTER_SIZE', + field: 'rafterSize', name: '垂木サイズ', }, { - field: 'RAFTER_PITCH', + field: 'rafterPitch', name: '垂木傾斜', }, { - field: 'WATERPROOF_MATERIAL', + field: 'waterproofMaterial', name: '防水材', }, { - field: 'INSULATION_PRESENCE', + field: 'insulationPresence', name: '断熱材有無', }, { - field: 'STRUCTURE_ORDER', + field: 'structureOrder', name: '屋根構造の順序', }, ] @@ -60,9 +56,8 @@ type ZipCode = { } export function useServey(id?: number): { - surveyList: SurveyBasicInfo[] | [] + surveyList: { data: SurveyBasicInfo[]; count: number } | {} surveyDetail: SurveyBasicInfo | null - surveyListCount: number isLoadingSurveyList: boolean isLoadingSurveyDetail: boolean isCreatingSurvey: boolean @@ -72,18 +67,23 @@ export function useServey(id?: number): { createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void updateSurvey: (survey: SurveyRegistRequest) => void deleteSurvey: () => Promise - submitSurvey: () => void + submitSurvey: (saveId?: number) => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise + refetchSurveyList: () => void } { const queryClient = useQueryClient() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() const { session } = useSessionStore() - const { data: surveyList, isLoading: isLoadingSurveyList } = useQuery({ + const { + data: surveyListData, + isLoading: isLoadingSurveyList, + refetch: refetchSurveyList, + } = useQuery({ queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderNo, session?.role], queryFn: async () => { - const resp = await axiosInstance(null).get('/api/survey-sales', { + const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { params: { keyword, searchOption, @@ -97,41 +97,30 @@ export function useServey(id?: number): { }) return resp.data }, + enabled: session?.isLoggedIn, }) + const surveyData = useMemo(() => { + if (!surveyListData) return { count: 0, data: [] } + return { + ...surveyListData, + } + }, [surveyListData]) const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({ queryKey: ['survey', id], queryFn: async () => { if (id === undefined) throw new Error('id is required') - if (id === null) return null + if (id === null || isNaN(id)) return null const resp = await axiosInstance(null).get(`/api/survey-sales/${id}`) return resp.data }, enabled: id !== undefined, }) - const { data: surveyListCount } = useQuery({ - queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, session?.builderNo, session?.storeNm, session?.role], - queryFn: async () => { - const resp = await axiosInstance(null).get('/api/survey-sales', { - params: { - keyword, - searchOption, - isMySurvey, - sort, - builderNo: session?.builderNo, - store: session?.storeNm, - role: session?.role, - }, - }) - return resp.data - }, - }) - const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { const resp = await axiosInstance(null).post('/api/survey-sales', survey) - return resp.data.ID ?? 0 + return resp.data.id ?? 0 }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) @@ -142,6 +131,7 @@ export function useServey(id?: number): { const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { + console.log('updateSurvey, survey:: ', survey) if (id === undefined) throw new Error('id is required') const resp = await axiosInstance(null).put(`/api/survey-sales/${id}`, survey) return resp.data @@ -176,9 +166,10 @@ export function useServey(id?: number): { }) const { mutateAsync: submitSurvey } = useMutation({ - mutationFn: async () => { - if (id === undefined) throw new Error('id is required') - const resp = await axiosInstance(null).patch(`/api/survey-sales/${id}`, { + mutationFn: async (saveId?: number) => { + const submitId = saveId ?? id + if (!submitId) throw new Error('id is required') + const resp = await axiosInstance(null).patch(`/api/survey-sales/${submitId}`, { submit: true, }) return resp.data @@ -190,24 +181,34 @@ export function useServey(id?: number): { }) const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => { - const etcFields = ['INSTALLATION_SYSTEM', 'CONSTRUCTION_YEAR', 'RAFTER_SIZE', 'RAFTER_PITCH', 'WATERPROOF_MATERIAL', 'STRUCTURE_ORDER'] as const + const etcFields = [ + 'installationSystem', + 'constructionYear', + 'rafterSize', + 'rafterPitch', + 'waterproofMaterial', + 'structureOrder', + 'insulationPresence', + ] as const const emptyField = requiredFields.find((field) => { if (etcFields.includes(field.field as (typeof etcFields)[number])) { return ( - surveyDetail[field.field as keyof SurveyDetailRequest] === null && surveyDetail[`${field.field}_ETC` as keyof SurveyDetailRequest] === '' + surveyDetail[field.field as keyof SurveyDetailRequest] === null && + (surveyDetail[`${field.field}Etc` as keyof SurveyDetailRequest] === null || + surveyDetail[`${field.field}Etc` as keyof SurveyDetailRequest]?.toString().trim() === '') ) } else { return surveyDetail[field.field as keyof SurveyDetailRequest] === null } }) - const contractCapacity = surveyDetail.CONTRACT_CAPACITY + const contractCapacity = surveyDetail.contractCapacity if (contractCapacity && contractCapacity.trim() !== '' && contractCapacity.split(' ')?.length === 1) { - return 'CONTRACT_CAPACITY_UNIT' + return 'contractCapacityUnit' } - return emptyField?.name || '' + return emptyField?.field || '' } const getZipCode = async (zipCode: string): Promise => { @@ -223,9 +224,8 @@ export function useServey(id?: number): { } return { - surveyList: surveyList || [], - surveyDetail: surveyDetail || null, - surveyListCount: surveyListCount || 0, + surveyList: surveyData.data, + surveyDetail: surveyDetail as SurveyBasicInfo | null, isLoadingSurveyList, isLoadingSurveyDetail, isCreatingSurvey, @@ -238,5 +238,6 @@ export function useServey(id?: number): { submitSurvey, validateSurveyDetail, getZipCode, + refetchSurveyList, } } diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 5f1b076..0abc6ab 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -2,37 +2,59 @@ import axios from 'axios' export const axiosInstance = (url: string | null | undefined) => { const baseURL = url || process.env.NEXT_PUBLIC_API_URL - - return axios.create({ + const instance = axios.create({ baseURL, headers: { Accept: 'application/json', }, }) + + instance.interceptors.request.use( + (config) => { + // console.log('🚀 ~ config:', config) + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + instance.interceptors.response.use( + (response) => { + response.data = transferResponse(response) + return response + }, + (error) => { + // 에러 처리 로직 + return Promise.reject(error) + }, + ) + + return instance } // Request interceptor -axios.interceptors.request.use( - (config) => { - // 여기에 토큰 추가 등의 공통 로직을 넣을 수 있습니다 - return config - }, - (error) => { - return Promise.reject(error) - }, -) +// axios.interceptors.request.use( +// (config) => { +// // 여기에 토큰 추가 등의 공통 로직을 넣을 수 있습니다 +// return config +// }, +// (error) => { +// return Promise.reject(error) +// }, +// ) // Response interceptor -axios.interceptors.response.use( - (response) => transferResponse(response), - (error) => { - // 에러 처리 로직 - return Promise.reject(error) - }, -) +// axios.interceptors.response.use( +// (response) => transferResponse(response), +// (error) => { +// // 에러 처리 로직 +// return Promise.reject(error) +// }, +// ) // response데이터가 array, object에 따라 분기하여 키 변환 -const transferResponse = (response: any) => { +export const transferResponse = (response: any) => { if (!response.data) return response.data // 배열인 경우 각 객체의 키를 변환 @@ -45,22 +67,34 @@ const transferResponse = (response: any) => { } // camel case object 반환 -const transformObjectKeys = (obj: any): any => { +export const transformObjectKeys = (obj: any): any => { if (Array.isArray(obj)) { return obj.map(transformObjectKeys) } if (obj !== null && typeof obj === 'object') { return Object.keys(obj).reduce((acc: any, key: string) => { - const camelKey = snakeToCamel(key) - acc[camelKey] = transformObjectKeys(obj[key]) + let transformedKey = key + + // Handle uppercase snake_case (e.g., USER_NAME -> userName) + // Handle lowercase snake_case (e.g., user_name -> userName) + if (/^[A-Z_]+$/.test(key) || /^[a-z_]+$/.test(key)) { + transformedKey = snakeToCamel(key) + } + // Handle single uppercase word (e.g., ROLE -> role) + else if (/^[A-Z]+$/.test(key)) { + transformedKey = key.toLowerCase() + } + // Preserve existing camelCase + + acc[transformedKey] = transformObjectKeys(obj[key]) return acc }, {}) } return obj } -// snake case to camel case + const snakeToCamel = (str: string): string => { - return str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) + return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', '')) } diff --git a/src/libs/partner.ts b/src/libs/partner.ts new file mode 100644 index 0000000..2b17677 --- /dev/null +++ b/src/libs/partner.ts @@ -0,0 +1,38 @@ +import { createPool } from 'mysql2' + +const pool = createPool({ + host: process.env.DB_HOST as string, + user: process.env.DB_USER as string, + password: process.env.DB_PASSWORD as string, + database: process.env.DB_DATABASE as string, + port: Number(process.env.DB_PORT), + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, +}) + +pool.getConnection((err, conn) => { + if (err) console.log('Error connecting to db...') + else console.log('Connected to db...!') + conn.release() +}) + +const executeQuery = (query: string, arrParams: any[]) => { + return new Promise((resolve, reject) => { + try { + pool.query(query, arrParams, (err, data) => { + if (err) { + console.log('🚀 ~ pool.query ~ err:', err) + reject(err) + } + console.log('🚀 ~ pool.query ~ data:', data) + resolve(data) + }) + } catch (err) { + console.log('🚀 ~ returnnewPromise ~ err:', err) + reject(err) + } + }) +} + +export default executeQuery diff --git a/src/libs/tracking.ts b/src/libs/tracking.ts new file mode 100644 index 0000000..2f8833f --- /dev/null +++ b/src/libs/tracking.ts @@ -0,0 +1,9 @@ +import { axiosInstance } from './axios' + +export const tracking = async (params: { url: string; data: string }) => { + const { url, data } = params + const result = await axiosInstance(null).post('/api/tracking', { + url, + data, + }) +} diff --git a/src/middleware.ts b/src/middleware.ts index 1bcfd96..115fd8a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,9 +10,9 @@ export async function middleware(request: NextRequest) { const session = await getIronSession(cookieStore, sessionOptions) // todo: 로그인 기능 추가 시 주석 해제 - // if (!session.isLoggedIn) { - // return NextResponse.redirect(new URL('/login', request.url)) - // } + if (!session.isLoggedIn) { + return NextResponse.redirect(new URL('/login', request.url)) + } return NextResponse.next() } diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index 4cfd67e..7bba179 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -1,12 +1,13 @@ 'use client' import { useEffect } from 'react' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useHeaderStore } from '@/store/header' import { usePopupController } from '@/store/popupController' import { useSideNavState } from '@/store/sideNavState' import { useSessionStore } from '@/store/session' +import { tracking } from '@/libs/tracking' declare global { interface Window { @@ -21,12 +22,30 @@ interface EdgeProviderProps { } export default function EdgeProvider({ children, sessionData }: EdgeProviderProps) { + const router = useRouter() const pathname = usePathname() const { setBackBtn } = useHeaderStore() const { reset } = useSideNavState() const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController() const { session, setSession } = useSessionStore() + if (pathname === '/login') { + if (session?.isLoggedIn) { + router.push('/') + } + } + + /** + * 사용자 이벤트 트래킹 처리 + * + */ + const handlePageEvent = (path: string) => { + tracking({ + url: path, + data: '', + }) + } + /** * alert 함수 - window.alert 함수 대체 * @param msg @@ -88,6 +107,8 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp } //사이드바 초기화 reset() + // 페이지 이벤트 트래킹 + handlePageEvent(pathname) }, [pathname]) return <>{children} diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts index 17b88c5..5fa4cd0 100644 --- a/src/store/useSuitableStore.ts +++ b/src/store/useSuitableStore.ts @@ -1,68 +1,71 @@ import { create } from 'zustand' -import { Suitable, suitableApi } from '@/api/suitable' +import type { CommCode } from '@/types/CommCode' interface SuitableState { - // // 검색 결과 리스트 - // searchResults: Suitable[] - // // 초기 데이터 로드 - // fetchInitializeData: () => Promise - // // 검색 결과 설정 - // setSearchResults: (results: Suitable[]) => void - // // 검색 결과 초기화 - // resetSearchResults: () => void + /* 공통코드 */ + suitableCommCode: Map + /* 공통코드 설정 */ + setSuitableCommCode: (headCode: string, commCode: CommCode[]) => void - // 선택된 아이템 리스트 - selectedItems: Suitable[] - // 선택된 아이템 추가 - addSelectedItem: (item: Suitable) => void - // 선택된 아이템 제거 + /* 검색 상태 */ + isSearch: boolean + /* 검색 상태 설정 */ + setIsSearch: (isSearch: boolean) => void + + /* 선택된 카테고리 */ + selectedCategory: string + /* 선택된 카테고리 설정 */ + setSelectedCategory: (category: string) => void + + /* 검색 값 */ + searchValue: string + /* 검색 값 설정 */ + setSearchValue: (value: string) => void + + /* 선택된 아이템 리스트 */ + selectedItems: number[] + /* 선택된 아이템 추가 */ + addSelectedItem: (itemId: number) => void + /* 선택된 아이템 제거 */ removeSelectedItem: (itemId: number) => void - // 선택된 아이템 모두 제거 + /* 선택된 아이템 모두 제거 */ clearSelectedItems: () => void } export const useSuitableStore = create((set) => ({ - // // 초기 상태 - // searchResults: [], + suitableCommCode: new Map() as Map, + isSearch: false as boolean, + selectedCategory: '' as string, + searchValue: '' as string, + selectedItems: [] as number[], - // // 초기 데이터 로드 - // fetchInitializeData: async () => { - // const suitables = await fetchInitialSuitablee() - // set({ searchResults: suitables }) - // }, - - // // 검색 결과 설정 - // setSearchResults: (results) => set({ searchResults: results }), - - // // 검색 결과 초기화 - // resetSearchResults: () => set({ searchResults: [] }), - - // 초기 상태 - selectedItems: [], - - // 선택된 아이템 추가 (중복 방지) - addSelectedItem: (item) => + /* 공통코드 설정 */ + setSuitableCommCode: (headCode: string, commCode: CommCode[]) => set((state) => ({ - selectedItems: state.selectedItems.some((i) => i.id === item.id) ? state.selectedItems : [...state.selectedItems, item], + suitableCommCode: new Map(state.suitableCommCode).set(headCode, commCode), })), - // 선택된 아이템 제거 - removeSelectedItem: (itemId) => + /* 검색 상태 설정 */ + setIsSearch: (isSearch: boolean) => set({ isSearch }), + + /* 선택된 카테고리 설정 */ + setSelectedCategory: (category: string) => set({ selectedCategory: category }), + + /* 검색 값 설정 */ + setSearchValue: (value: string) => set({ searchValue: value }), + + /* 선택된 아이템 추가 */ + addSelectedItem: (itemId: number) => set((state) => ({ - selectedItems: state.selectedItems.filter((item) => item.id !== itemId), + selectedItems: state.selectedItems.some((i) => i === itemId) ? state.selectedItems : [...state.selectedItems, itemId], })), - // 선택된 아이템 모두 제거 + /* 선택된 아이템 제거 */ + removeSelectedItem: (itemId: number) => + set((state) => ({ + selectedItems: state.selectedItems.filter((i) => i !== itemId), + })), + + /* 선택된 아이템 모두 제거 */ clearSelectedItems: () => set({ selectedItems: [] }), })) - -// // 전체 데이터 초기화 함수 -// async function fetchInitialSuitablee() { -// try { -// const suitable = await suitableApi.getList() -// return suitable -// } catch (error) { -// console.error('초기 데이터 로드 실패:', error) -// return [] -// } -// } diff --git a/src/types/CommCode.ts b/src/types/CommCode.ts new file mode 100644 index 0000000..5847047 --- /dev/null +++ b/src/types/CommCode.ts @@ -0,0 +1,5 @@ +export type CommCode = { + headCd: string + code: string + codeJp: string +} diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts new file mode 100644 index 0000000..2e3563b --- /dev/null +++ b/src/types/Suitable.ts @@ -0,0 +1,44 @@ +export enum SUITABLE_HEAD_CODE { + /* 지붕재 제조사명 */ + MANU_FT_CD = 'MANU_FT_CD', + /* 지붕재 종류 */ + ROOF_MT_CD = 'ROOF_MT_CD', + /* 마운팅 브래킷 종류 */ + ROOF_SH_CD = 'ROOF_SH_CD', + /* 마운팅 브래킷 제조사명 및 제품코드드 */ + TRESTLE_MFPC_CD = 'TRESTLE_MFPC_CD', +} + +export type SuitableMain = { + id: number + productName: string + manuFtCd: string + roofMtCd: string + roofShCd: string +} + +export type SuitableDetail = { + id: number + mainId: number + trestleMfpcCd: string + trestleManufacturerProductName: string + memo: string +} + +// export type Suitable = { +// id: number +// productName: string +// manuFtCd: string +// roofMtCd: string +// roofShCd: string +// detail: string +// } + +export type SuitableDetailGroup = { + mainId: number + detail: string +} +export type Suitable = { + suitable: SuitableMain[] + detail: SuitableDetailGroup[] +} \ No newline at end of file diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 2cfbbec..36e7aa5 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -1,130 +1,132 @@ export type SurveyBasicInfo = { - ID: number - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null - DETAIL_INFO: SurveyDetailInfo | null - REG_DT: Date - UPT_DT: Date + id: number + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null + detailInfo: SurveyDetailInfo | null + regDt: Date + uptDt: Date } export type SurveyDetailInfo = { - ID: number - BASIC_INFO_ID: number - CONTRACT_CAPACITY: string | null - RETAIL_COMPANY: string | null - SUPPLEMENTARY_FACILITIES: string | null // number 배열 - SUPPLEMENTARY_FACILITIES_ETC: string | null - INSTALLATION_SYSTEM: string | null - INSTALLATION_SYSTEM_ETC: string | null - CONSTRUCTION_YEAR: string | null - CONSTRUCTION_YEAR_ETC: string | null - ROOF_MATERIAL: string | null // number 배열 - ROOF_MATERIAL_ETC: string | null - ROOF_SHAPE: string | null - ROOF_SHAPE_ETC: string | null - ROOF_SLOPE: string | null - HOUSE_STRUCTURE: string | null - HOUSE_STRUCTURE_ETC: string | null - RAFTER_MATERIAL: string | null - RAFTER_MATERIAL_ETC: string | null - RAFTER_SIZE: string | null - RAFTER_SIZE_ETC: string | null - RAFTER_PITCH: string | null - RAFTER_PITCH_ETC: string | null - RAFTER_DIRECTION: string | null - OPEN_FIELD_PLATE_KIND: string | null - OPEN_FIELD_PLATE_KIND_ETC: string | null - OPEN_FIELD_PLATE_THICKNESS: string | null - LEAK_TRACE: boolean | null - WATERPROOF_MATERIAL: string | null - WATERPROOF_MATERIAL_ETC: string | null - INSULATION_PRESENCE: string | null - INSULATION_PRESENCE_ETC: string | null - STRUCTURE_ORDER: string | null - STRUCTURE_ORDER_ETC: string | null - INSTALLATION_AVAILABILITY: string | null - INSTALLATION_AVAILABILITY_ETC: string | null - MEMO: string | null - REG_DT: Date - UPT_DT: Date + id: number + basicInfoId: number + contractCapacity: string | null + retailCompany: string | null + supplementaryFacilities: string | null // number 배열 + supplementaryFacilitiesEtc: string | null + installationSystem: string | null + installationSystemEtc: string | null + constructionYear: string | null + constructionYearEtc: string | null + roofMaterial: string | null // number 배열 + roofMaterialEtc: string | null + roofShape: string | null + roofShapeEtc: string | null + roofSlope: string | null + houseStructure: string | null + houseStructureEtc: string | null + rafterMaterial: string | null + rafterMaterialEtc: string | null + rafterSize: string | null + rafterSizeEtc: string | null + rafterPitch: string | null + rafterPitchEtc: string | null + rafterDirection: string | null + openFieldPlateKind: string | null + openFieldPlateKindEtc: string | null + openFieldPlateThickness: string | null + leakTrace: boolean | null + waterproofMaterial: string | null + waterproofMaterialEtc: string | null + insulationPresence: string | null + insulationPresenceEtc: string | null + structureOrder: string | null + structureOrderEtc: string | null + installationAvailability: string | null + installationAvailabilityEtc: string | null + memo: string | null + regDt: Date + uptDt: Date } export type SurveyBasicRequest = { - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null } export type SurveyDetailRequest = { - CONTRACT_CAPACITY: string | null - RETAIL_COMPANY: string | null - SUPPLEMENTARY_FACILITIES: string | null // number 배열 - SUPPLEMENTARY_FACILITIES_ETC: string | null - INSTALLATION_SYSTEM: string | null - INSTALLATION_SYSTEM_ETC: string | null - CONSTRUCTION_YEAR: string | null - CONSTRUCTION_YEAR_ETC: string | null - ROOF_MATERIAL: string | null // number 배열 - ROOF_MATERIAL_ETC: string | null - ROOF_SHAPE: string | null - ROOF_SHAPE_ETC: string | null - ROOF_SLOPE: string | null - HOUSE_STRUCTURE: string | null - HOUSE_STRUCTURE_ETC: string | null - RAFTER_MATERIAL: string | null - RAFTER_MATERIAL_ETC: string | null - RAFTER_SIZE: string | null - RAFTER_SIZE_ETC: string | null - RAFTER_PITCH: string | null - RAFTER_PITCH_ETC: string | null - RAFTER_DIRECTION: string | null - OPEN_FIELD_PLATE_KIND: string | null - OPEN_FIELD_PLATE_KIND_ETC: string | null - OPEN_FIELD_PLATE_THICKNESS: string | null - LEAK_TRACE: boolean | null - WATERPROOF_MATERIAL: string | null - WATERPROOF_MATERIAL_ETC: string | null - INSULATION_PRESENCE: string | null - INSULATION_PRESENCE_ETC: string | null - STRUCTURE_ORDER: string | null - STRUCTURE_ORDER_ETC: string | null - INSTALLATION_AVAILABILITY: string | null - INSTALLATION_AVAILABILITY_ETC: string | null - MEMO: string | null + contractCapacity: string | null + retailCompany: string | null + supplementaryFacilities: string | null // number 배열 + supplementaryFacilitiesEtc: string | null + installationSystem: string | null + installationSystemEtc: string | null + constructionYear: string | null + constructionYearEtc: string | null + roofMaterial: string | null // number 배열 + roofMaterialEtc: string | null + roofShape: string | null + roofShapeEtc: string | null + roofSlope: string | null + houseStructure: string | null + houseStructureEtc: string | null + rafterMaterial: string | null + rafterMaterialEtc: string | null + rafterSize: string | null + rafterSizeEtc: string | null + rafterPitch: string | null + rafterPitchEtc: string | null + rafterDirection: string | null + openFieldPlateKind: string | null + openFieldPlateKindEtc: string | null + openFieldPlateThickness: string | null + leakTrace: boolean | null + waterproofMaterial: string | null + waterproofMaterialEtc: string | null + insulationPresence: string | null + insulationPresenceEtc: string | null + structureOrder: string | null + structureOrderEtc: string | null + installationAvailability: string | null + installationAvailabilityEtc: string | null + memo: string | null } export type SurveyDetailCoverRequest = { - DETAIL_INFO: SurveyDetailRequest + detailInfo: SurveyDetailRequest } export type SurveyRegistRequest = { - REPRESENTATIVE: string - STORE: string | null - CONSTRUCTION_POINT: string | null - INVESTIGATION_DATE: string | null - BUILDING_NAME: string | null - CUSTOMER_NAME: string | null - POST_CODE: string | null - ADDRESS: string | null - ADDRESS_DETAIL: string | null - SUBMISSION_STATUS: boolean - SUBMISSION_DATE: string | null - DETAIL_INFO: SurveyDetailRequest | null + representative: string + store: string | null + constructionPoint: string | null + investigationDate: string | null + buildingName: string | null + customerName: string | null + postCode: string | null + address: string | null + addressDetail: string | null + submissionStatus: boolean + submissionDate: string | null + detailInfo: SurveyDetailRequest | null } + +export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 diff --git a/src/utils/common-utils.js b/src/utils/common-utils.js index 0a1265f..d0c7f65 100644 --- a/src/utils/common-utils.js +++ b/src/utils/common-utils.js @@ -185,3 +185,28 @@ export const isEqualObjects = (obj1, obj2) => { function isObject(value) { return value !== null && typeof value === 'object' } + + +// 카멜케이스를 스네이크케이스로 변환하는 함수 +export const toSnakeCase = (str) => { + return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +} + +// 객체의 키를 스네이크케이스로 변환하는 함수 +export const convertToSnakeCase = (obj) => { + if (obj === null || obj === undefined) return obj; + + if (Array.isArray(obj)) { + return obj.map((item) => convertToSnakeCase(item)) + } + + if (typeof obj === 'object') { + return Object.keys(obj).reduce((acc, key) => { + const snakeKey = toSnakeCase(key).toUpperCase(); + acc[snakeKey] = convertToSnakeCase(obj[key]); + return acc; + }, {}); + } + + return obj; +}