release: Phase 1~5 리팩토링 통합 릴리즈 #26
@ -6,9 +6,9 @@
|
||||
|
||||
- **프로젝트 타입**: react-ts (모노레포)
|
||||
- **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
|
||||
- **Backend**: Express 4 + better-sqlite3 + TypeScript
|
||||
- **Backend**: Express 4 + PostgreSQL (pg) + TypeScript
|
||||
- **상태관리**: Zustand (클라이언트), TanStack Query (서버)
|
||||
- **지도**: Leaflet, OpenLayers
|
||||
- **지도**: Leaflet
|
||||
- **실시간**: Socket.IO
|
||||
|
||||
## 빌드/실행
|
||||
@ -70,7 +70,7 @@ wing/
|
||||
│ ├── audit/ 감사 로그
|
||||
│ ├── routes/ 레이어, 시뮬레이션
|
||||
│ ├── middleware/ 보안 (입력 살균, rate-limit)
|
||||
│ └── db/ DB 연결 (PostgreSQL, SQLite)
|
||||
│ └── db/ DB 연결 (PostgreSQL: wing, wing_auth)
|
||||
├── database/ SQL 초기화 스크립트
|
||||
├── docs/ 개발 문서 (README, 가이드, 변경이력)
|
||||
├── .claude/ 팀 워크플로우 (rules, skills, scripts)
|
||||
|
||||
@ -126,7 +126,7 @@ wing/
|
||||
| 영역 | 기술 |
|
||||
|------|------|
|
||||
| Frontend | React 19, Vite 7, TypeScript 5.9, Tailwind CSS 3 |
|
||||
| Backend | Express 4, TypeScript, better-sqlite3 (레이어), pg (인증) |
|
||||
| Backend | Express 4, TypeScript, PostgreSQL (pg) |
|
||||
| 상태 관리 | Zustand (클라이언트), TanStack Query (서버) |
|
||||
| 지도 | Leaflet, OpenLayers |
|
||||
| 실시간 | Socket.IO |
|
||||
|
||||
402
backend/package-lock.json
generated
402
backend/package-lock.json
generated
@ -9,7 +9,6 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.3.1",
|
||||
@ -22,7 +21,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
@ -513,16 +511,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@ -773,17 +761,6 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
@ -793,26 +770,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@ -873,30 +830,6 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@ -941,12 +874,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@ -1077,30 +1004,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@ -1120,15 +1023,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||
@ -1191,15 +1085,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@ -1287,15 +1172,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
@ -1404,12 +1280,6 @@
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@ -1489,12 +1359,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -1598,12 +1462,6 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@ -1729,38 +1587,12 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
@ -1978,18 +1810,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
@ -2005,15 +1825,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
@ -2023,24 +1834,12 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@ -2050,18 +1849,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.87.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
|
||||
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
@ -2153,15 +1940,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@ -2336,32 +2114,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -2375,16 +2127,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
@ -2436,35 +2178,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
@ -2693,51 +2406,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@ -2756,15 +2424,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@ -2861,43 +2520,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@ -2927,18 +2549,6 @@
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@ -2982,12 +2592,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@ -3121,12 +2725,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.3.1",
|
||||
@ -23,7 +22,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import Database from 'better-sqlite3'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const dbPath = join(__dirname, '../../data/layers.db')
|
||||
|
||||
export const db = new Database(dbPath)
|
||||
|
||||
// 데이터베이스 초기화
|
||||
export function initDatabase() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS layers (
|
||||
cmn_cd TEXT PRIMARY KEY,
|
||||
up_cmn_cd TEXT,
|
||||
cmn_cd_full_nm TEXT NOT NULL,
|
||||
cmn_cd_nm TEXT NOT NULL,
|
||||
cmn_cd_level INTEGER NOT NULL,
|
||||
clnm TEXT,
|
||||
FOREIGN KEY (up_cmn_cd) REFERENCES layers(cmn_cd)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_up_cmn_cd ON layers(up_cmn_cd);
|
||||
CREATE INDEX IF NOT EXISTS idx_cmn_cd_level ON layers(cmn_cd_level);
|
||||
`)
|
||||
}
|
||||
|
||||
export default db
|
||||
@ -1,91 +1,106 @@
|
||||
import 'dotenv/config'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
import db, { initDatabase } from './database.js'
|
||||
import { wingPool } from './wingDb.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
async function seedDatabase() {
|
||||
console.log('데이터베이스 초기화 중...')
|
||||
initDatabase()
|
||||
console.log('wing DB 레이어 시드 시작...')
|
||||
|
||||
// 기존 데이터 삭제
|
||||
db.exec('DELETE FROM layers')
|
||||
const client = await wingPool.connect()
|
||||
|
||||
// CSV 파일 읽기
|
||||
const csvPath = path.join(__dirname, '../../../LayerList.csv')
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
||||
|
||||
// CSV 파싱
|
||||
const lines = csvContent.split('\n')
|
||||
const headers = lines[0].split(',').map(h => h.replace(/"/g, '').trim())
|
||||
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO layers (cmn_cd, up_cmn_cd, cmn_cd_full_nm, cmn_cd_nm, cmn_cd_level, clnm)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
try {
|
||||
// CSV 파일 읽기
|
||||
const csvPath = path.join(__dirname, '../../../_reference/LayerList.csv')
|
||||
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
||||
|
||||
const insertMany = db.transaction((rows: any[]) => {
|
||||
for (const row of rows) {
|
||||
insert.run(row)
|
||||
}
|
||||
})
|
||||
// CSV 파싱
|
||||
const lines = csvContent.split('\n')
|
||||
|
||||
const rows = []
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) continue
|
||||
const rows: (string | number | null)[][] = []
|
||||
|
||||
// CSV 파싱 (쉼표로 구분, 따옴표 처리)
|
||||
const values = []
|
||||
let current = ''
|
||||
let inQuotes = false
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) continue
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j]
|
||||
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
values.push(current.trim())
|
||||
current = ''
|
||||
} else {
|
||||
current += char
|
||||
const values: string[] = []
|
||||
let current = ''
|
||||
let inQuotes = false
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j]
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
values.push(current.trim())
|
||||
current = ''
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
values.push(current.trim())
|
||||
|
||||
const row = values.map(v => {
|
||||
if (v === 'NULL' || v === '') return null
|
||||
return v.replace(/"/g, '')
|
||||
})
|
||||
|
||||
if (row.length >= 6) {
|
||||
rows.push([
|
||||
row[0], // LAYER_CD
|
||||
row[1], // UP_LAYER_CD
|
||||
row[2], // LAYER_FULL_NM
|
||||
row[3], // LAYER_NM
|
||||
parseInt(row[4] || '0', 10), // LAYER_LEVEL
|
||||
row[5], // WMS_LAYER_NM
|
||||
])
|
||||
}
|
||||
}
|
||||
values.push(current.trim())
|
||||
|
||||
// NULL 값 처리
|
||||
const row = values.map(v => {
|
||||
if (v === 'NULL' || v === '') return null
|
||||
return v.replace(/"/g, '')
|
||||
})
|
||||
console.log(`${rows.length}개의 레이어 데이터 삽입 중...`)
|
||||
|
||||
if (row.length >= 6) {
|
||||
rows.push([
|
||||
row[0], // cmn_cd
|
||||
row[1], // up_cmn_cd
|
||||
row[2], // cmn_cd_full_nm
|
||||
row[3], // cmn_cd_nm
|
||||
parseInt(row[4] || '0'), // cmn_cd_level
|
||||
row[5], // clnm
|
||||
])
|
||||
await client.query('BEGIN')
|
||||
|
||||
// 기존 데이터 삭제
|
||||
await client.query('DELETE FROM LAYER')
|
||||
|
||||
// FK 제약 때문에 상위 레이어(낮은 레벨)부터 삽입
|
||||
const sortedRows = rows.sort((a, b) => (a[4] as number) - (b[4] as number))
|
||||
|
||||
for (const row of sortedRows) {
|
||||
await client.query(
|
||||
`INSERT INTO LAYER (LAYER_CD, UP_LAYER_CD, LAYER_FULL_NM, LAYER_NM, LAYER_LEVEL, WMS_LAYER_NM)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (LAYER_CD) DO UPDATE SET
|
||||
UP_LAYER_CD = EXCLUDED.UP_LAYER_CD,
|
||||
LAYER_FULL_NM = EXCLUDED.LAYER_FULL_NM,
|
||||
LAYER_NM = EXCLUDED.LAYER_NM,
|
||||
LAYER_LEVEL = EXCLUDED.LAYER_LEVEL,
|
||||
WMS_LAYER_NM = EXCLUDED.WMS_LAYER_NM`,
|
||||
row
|
||||
)
|
||||
}
|
||||
|
||||
await client.query('COMMIT')
|
||||
|
||||
// 결과 확인
|
||||
const { rows: countResult } = await client.query('SELECT COUNT(*) as count FROM LAYER')
|
||||
console.log(`시드 완료! 총 ${countResult[0].count}개의 레이어가 저장되었습니다.`)
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK')
|
||||
console.error('시드 실패:', err)
|
||||
throw err
|
||||
} finally {
|
||||
client.release()
|
||||
await wingPool.end()
|
||||
}
|
||||
|
||||
console.log(`${rows.length}개의 레이어 데이터 삽입 중...`)
|
||||
insertMany(rows)
|
||||
|
||||
console.log('시드 완료!')
|
||||
|
||||
// 결과 확인
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM layers').get() as { count: number }
|
||||
console.log(`총 ${count.count}개의 레이어가 저장되었습니다.`)
|
||||
|
||||
db.close()
|
||||
}
|
||||
|
||||
seedDatabase().catch(console.error)
|
||||
seedDatabase().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
33
backend/src/db/wingDb.ts
Normal file
33
backend/src/db/wingDb.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import pg from 'pg'
|
||||
|
||||
const { Pool } = pg
|
||||
|
||||
const wingPool = new Pool({
|
||||
host: process.env.WING_DB_HOST || 'localhost',
|
||||
port: Number(process.env.WING_DB_PORT) || 5432,
|
||||
database: process.env.WING_DB_NAME || 'wing',
|
||||
user: process.env.WING_DB_USER || 'wing',
|
||||
password: process.env.WING_DB_PASSWORD || 'Wing2026',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
})
|
||||
|
||||
wingPool.on('error', (err) => {
|
||||
console.error('[wingDb] 예기치 않은 연결 오류:', err.message)
|
||||
})
|
||||
|
||||
export async function testWingDbConnection(): Promise<boolean> {
|
||||
try {
|
||||
const client = await wingPool.connect()
|
||||
await client.query('SELECT 1')
|
||||
client.release()
|
||||
console.log('[wingDb] wing 데이터베이스 연결 성공')
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn('[wingDb] wing 데이터베이스 연결 실패:', (err as Error).message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export { wingPool }
|
||||
@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import db from '../db/database.js'
|
||||
import { wingPool } from '../db/wingDb.js'
|
||||
import { enrichLayerWithMetadata } from '../utils/layerIcons.js'
|
||||
import {
|
||||
sanitizeParams,
|
||||
@ -19,14 +19,26 @@ interface Layer {
|
||||
clnm: string | null
|
||||
}
|
||||
|
||||
// DB 컬럼 → API 응답 컬럼 매핑 (프론트엔드 호환성 유지)
|
||||
const LAYER_COLUMNS = `
|
||||
LAYER_CD AS cmn_cd,
|
||||
UP_LAYER_CD AS up_cmn_cd,
|
||||
LAYER_FULL_NM AS cmn_cd_full_nm,
|
||||
LAYER_NM AS cmn_cd_nm,
|
||||
LAYER_LEVEL AS cmn_cd_level,
|
||||
WMS_LAYER_NM AS clnm
|
||||
`.trim()
|
||||
|
||||
// 모든 라우트에 파라미터 살균 적용
|
||||
router.use(sanitizeParams)
|
||||
|
||||
// 모든 레이어 조회
|
||||
router.get('/', (_req, res) => {
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const layers = db.prepare('SELECT * FROM layers ORDER BY cmn_cd').all() as Layer[]
|
||||
const enrichedLayers = layers.map(enrichLayerWithMetadata)
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD`
|
||||
)
|
||||
const enrichedLayers = rows.map(enrichLayerWithMetadata)
|
||||
res.json(enrichedLayers)
|
||||
} catch {
|
||||
res.status(500).json({ error: '레이어 조회 실패' })
|
||||
@ -34,17 +46,19 @@ router.get('/', (_req, res) => {
|
||||
})
|
||||
|
||||
// 계층 구조로 변환된 레이어 트리 조회
|
||||
router.get('/tree/all', (_req, res) => {
|
||||
router.get('/tree/all', async (_req, res) => {
|
||||
try {
|
||||
const layers = db.prepare('SELECT * FROM layers ORDER BY cmn_cd').all() as Layer[]
|
||||
const enrichedLayers = layers.map(enrichLayerWithMetadata)
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD`
|
||||
)
|
||||
const enrichedLayers = rows.map(enrichLayerWithMetadata)
|
||||
|
||||
const layerMap = new Map<string, any>()
|
||||
const layerMap = new Map<string, Layer & { children: Layer[] }>()
|
||||
enrichedLayers.forEach(layer => {
|
||||
layerMap.set(layer.cmn_cd, { ...layer, children: [] })
|
||||
})
|
||||
|
||||
const rootLayers: any[] = []
|
||||
const rootLayers: (Layer & { children: Layer[] })[] = []
|
||||
enrichedLayers.forEach(layer => {
|
||||
const layerNode = layerMap.get(layer.cmn_cd)!
|
||||
if (layer.up_cmn_cd === null) {
|
||||
@ -64,10 +78,12 @@ router.get('/tree/all', (_req, res) => {
|
||||
})
|
||||
|
||||
// WMS 레이어만 조회
|
||||
router.get('/wms/all', (_req, res) => {
|
||||
router.get('/wms/all', async (_req, res) => {
|
||||
try {
|
||||
const layers = db.prepare('SELECT * FROM layers WHERE clnm IS NOT NULL ORDER BY cmn_cd').all() as Layer[]
|
||||
const enrichedLayers = layers.map(enrichLayerWithMetadata)
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE WMS_LAYER_NM IS NOT NULL AND USE_YN = 'Y' ORDER BY LAYER_CD`
|
||||
)
|
||||
const enrichedLayers = rows.map(enrichLayerWithMetadata)
|
||||
res.json(enrichedLayers)
|
||||
} catch {
|
||||
res.status(500).json({ error: 'WMS 레이어 조회 실패' })
|
||||
@ -75,11 +91,10 @@ router.get('/wms/all', (_req, res) => {
|
||||
})
|
||||
|
||||
// 특정 레벨의 레이어만 조회
|
||||
router.get('/level/:level', (req, res) => {
|
||||
router.get('/level/:level', async (req, res) => {
|
||||
try {
|
||||
const level = parseInt(req.params.level, 10)
|
||||
|
||||
// 입력 검증: 레벨은 1~10 범위의 정수
|
||||
if (!isValidNumber(level, 1, 10)) {
|
||||
return res.status(400).json({
|
||||
error: '유효하지 않은 레벨값',
|
||||
@ -87,9 +102,11 @@ router.get('/level/:level', (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 파라미터화된 쿼리 사용 (SQL 인젝션 방지)
|
||||
const layers = db.prepare('SELECT * FROM layers WHERE cmn_cd_level = ? ORDER BY cmn_cd').all(level) as Layer[]
|
||||
const enrichedLayers = layers.map(enrichLayerWithMetadata)
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE LAYER_LEVEL = $1 AND USE_YN = 'Y' ORDER BY LAYER_CD`,
|
||||
[level]
|
||||
)
|
||||
const enrichedLayers = rows.map(enrichLayerWithMetadata)
|
||||
res.json(enrichedLayers)
|
||||
} catch {
|
||||
res.status(500).json({ error: '레벨별 레이어 조회 실패' })
|
||||
@ -97,11 +114,10 @@ router.get('/level/:level', (req, res) => {
|
||||
})
|
||||
|
||||
// 특정 부모의 자식 레이어 조회
|
||||
router.get('/children/:parentId', (req, res) => {
|
||||
router.get('/children/:parentId', async (req, res) => {
|
||||
try {
|
||||
const parentId = req.params.parentId
|
||||
|
||||
// 입력 검증: 코드 형식 확인 (영숫자, 언더스코어, 하이픈만 허용)
|
||||
if (!parentId || !isValidStringLength(parentId, 50) || !/^[a-zA-Z0-9_-]+$/.test(parentId)) {
|
||||
return res.status(400).json({
|
||||
error: '유효하지 않은 부모 ID',
|
||||
@ -110,8 +126,11 @@ router.get('/children/:parentId', (req, res) => {
|
||||
}
|
||||
|
||||
const sanitizedId = sanitizeString(parentId)
|
||||
const layers = db.prepare('SELECT * FROM layers WHERE up_cmn_cd = ? ORDER BY cmn_cd').all(sanitizedId) as Layer[]
|
||||
const enrichedLayers = layers.map(enrichLayerWithMetadata)
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE UP_LAYER_CD = $1 AND USE_YN = 'Y' ORDER BY LAYER_CD`,
|
||||
[sanitizedId]
|
||||
)
|
||||
const enrichedLayers = rows.map(enrichLayerWithMetadata)
|
||||
res.json(enrichedLayers)
|
||||
} catch {
|
||||
res.status(500).json({ error: '자식 레이어 조회 실패' })
|
||||
@ -119,11 +138,10 @@ router.get('/children/:parentId', (req, res) => {
|
||||
})
|
||||
|
||||
// 특정 레이어 조회
|
||||
router.get('/:id', (req, res) => {
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id
|
||||
|
||||
// 입력 검증: ID 형식 확인
|
||||
if (!id || !isValidStringLength(id, 50) || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
||||
return res.status(400).json({
|
||||
error: '유효하지 않은 레이어 ID',
|
||||
@ -132,11 +150,14 @@ router.get('/:id', (req, res) => {
|
||||
}
|
||||
|
||||
const sanitizedId = sanitizeString(id)
|
||||
const layer = db.prepare('SELECT * FROM layers WHERE cmn_cd = ?').get(sanitizedId) as Layer | undefined
|
||||
if (!layer) {
|
||||
const { rows } = await wingPool.query<Layer>(
|
||||
`SELECT ${LAYER_COLUMNS} FROM LAYER WHERE LAYER_CD = $1`,
|
||||
[sanitizedId]
|
||||
)
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: '레이어를 찾을 수 없습니다' })
|
||||
}
|
||||
const enrichedLayer = enrichLayerWithMetadata(layer)
|
||||
const enrichedLayer = enrichLayerWithMetadata(rows[0])
|
||||
res.json(enrichedLayer)
|
||||
} catch {
|
||||
res.status(500).json({ error: '레이어 조회 실패' })
|
||||
|
||||
@ -4,8 +4,8 @@ import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import { initDatabase } from './db/database.js'
|
||||
import { testAuthDbConnection } from './db/authDb.js'
|
||||
import { testWingDbConnection } from './db/wingDb.js'
|
||||
import layersRouter from './routes/layers.js'
|
||||
import simulationRouter from './routes/simulation.js'
|
||||
import authRouter from './auth/authRouter.js'
|
||||
@ -113,11 +113,6 @@ app.use(express.urlencoded({ extended: false, limit: BODY_SIZE_LIMIT }))
|
||||
app.use(sanitizeBody)
|
||||
app.use(sanitizeQuery)
|
||||
|
||||
// ============================================================
|
||||
// 데이터베이스 초기화
|
||||
// ============================================================
|
||||
initDatabase()
|
||||
|
||||
// ============================================================
|
||||
// 라우트
|
||||
// ============================================================
|
||||
@ -176,6 +171,11 @@ app.use((err: Error, _req: express.Request, res: express.Response, _next: expres
|
||||
// ============================================================
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`서버가 포트 ${PORT}에서 실행 중입니다.`)
|
||||
|
||||
// wing DB (운영 데이터) 연결 확인
|
||||
await testWingDbConnection()
|
||||
|
||||
// wing_auth DB (인증 데이터) 연결 확인
|
||||
const connected = await testAuthDbConnection()
|
||||
if (connected) {
|
||||
// SETTING_VAL VARCHAR(500) → TEXT 마이그레이션 (메뉴 설정 JSON 확장 대응)
|
||||
|
||||
36
database/migration/001_layer_table.sql
Normal file
36
database/migration/001_layer_table.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- ================================================================
|
||||
-- 001: LAYER 테이블 생성 (SQLite layers.db → PostgreSQL wing DB 마이그레이션)
|
||||
-- ================================================================
|
||||
-- 기존 SQLite layers 테이블의 데이터를 PostgreSQL wing DB로 이전
|
||||
-- 공공데이터베이스 표준화 관리 매뉴얼(2021.06) 네이밍 적용
|
||||
-- ================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS LAYER (
|
||||
LAYER_CD VARCHAR(50) NOT NULL, -- 레이어코드 (기존 cmn_cd)
|
||||
UP_LAYER_CD VARCHAR(50), -- 상위레이어코드 (기존 up_cmn_cd)
|
||||
LAYER_FULL_NM VARCHAR(200) NOT NULL, -- 레이어전체명 (기존 cmn_cd_full_nm)
|
||||
LAYER_NM VARCHAR(100) NOT NULL, -- 레이어명 (기존 cmn_cd_nm)
|
||||
LAYER_LEVEL INTEGER NOT NULL, -- 레이어레벨 (기존 cmn_cd_level)
|
||||
WMS_LAYER_NM VARCHAR(100), -- WMS레이어명 (기존 clnm)
|
||||
USE_YN CHAR(1) NOT NULL DEFAULT 'Y', -- 사용여부
|
||||
SORT_ORD INTEGER DEFAULT 0, -- 정렬순서
|
||||
REG_DTM TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 등록일시
|
||||
CONSTRAINT PK_LAYER PRIMARY KEY (LAYER_CD),
|
||||
CONSTRAINT FK_LAYER_UP FOREIGN KEY (UP_LAYER_CD) REFERENCES LAYER(LAYER_CD),
|
||||
CONSTRAINT CK_LAYER_USE_YN CHECK (USE_YN IN ('Y', 'N'))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE LAYER IS '레이어';
|
||||
COMMENT ON COLUMN LAYER.LAYER_CD IS '레이어코드 (예: LYR001001)';
|
||||
COMMENT ON COLUMN LAYER.UP_LAYER_CD IS '상위레이어코드 (상위 레이어 참조)';
|
||||
COMMENT ON COLUMN LAYER.LAYER_FULL_NM IS '레이어전체명 (계층 경로 포함 전체 명칭)';
|
||||
COMMENT ON COLUMN LAYER.LAYER_NM IS '레이어명 (표시용 짧은 명칭)';
|
||||
COMMENT ON COLUMN LAYER.LAYER_LEVEL IS '레이어레벨 (1:최상위, 2:중분류, 3:소분류 ...)';
|
||||
COMMENT ON COLUMN LAYER.WMS_LAYER_NM IS 'WMS레이어명 (GeoServer WMS 레이어 식별자)';
|
||||
COMMENT ON COLUMN LAYER.USE_YN IS '사용여부 (Y:사용, N:미사용)';
|
||||
COMMENT ON COLUMN LAYER.SORT_ORD IS '정렬순서';
|
||||
COMMENT ON COLUMN LAYER.REG_DTM IS '등록일시';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_LAYER_UP ON LAYER(UP_LAYER_CD);
|
||||
CREATE INDEX IF NOT EXISTS IDX_LAYER_LEVEL ON LAYER(LAYER_LEVEL);
|
||||
CREATE INDEX IF NOT EXISTS IDX_LAYER_USE ON LAYER(USE_YN);
|
||||
@ -291,14 +291,13 @@ const mutation = useMutation({
|
||||
|
||||
### DB 접근
|
||||
```typescript
|
||||
// PostgreSQL (인증 DB)
|
||||
import { authPool } from '../db/authDb.js'
|
||||
const result = await authPool.query('SELECT * FROM TABLE WHERE id = $1', [id])
|
||||
// PostgreSQL — wing DB (운영 데이터: 레이어, 사고, 예측 등)
|
||||
import { wingPool } from '../db/wingDb.js'
|
||||
const result = await wingPool.query('SELECT * FROM LAYER WHERE LAYER_CD = $1', [id])
|
||||
|
||||
// SQLite (레이어 DB)
|
||||
import { getDb } from '../db/database.js'
|
||||
const db = getDb()
|
||||
const rows = db.prepare('SELECT * FROM table').all()
|
||||
// PostgreSQL — wing_auth DB (인증 데이터: 사용자, 역할, 권한 등)
|
||||
import { authPool } from '../db/authDb.js'
|
||||
const result = await authPool.query('SELECT * FROM AUTH_USER WHERE USER_ID = $1', [id])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -34,12 +34,12 @@ claude
|
||||
| 영역 | 기술 |
|
||||
|------|------|
|
||||
| Frontend | React 19, Vite 7, TypeScript 5.9, Tailwind CSS 3 |
|
||||
| Backend | Express 4, TypeScript, better-sqlite3 (레이어), pg (인증) |
|
||||
| Backend | Express 4, TypeScript, PostgreSQL (pg) |
|
||||
| 상태 관리 | Zustand (클라이언트), TanStack Query (서버) |
|
||||
| 지도 | Leaflet, OpenLayers |
|
||||
| 실시간 | Socket.IO |
|
||||
| 인증 | JWT (HttpOnly Cookie), Google OAuth |
|
||||
| DB | PostgreSQL 16 + PostGIS (운영 DB 직접 연결), SQLite |
|
||||
| DB | PostgreSQL 16 + PostGIS (wing + wing_auth) |
|
||||
| CI/CD | Gitea Actions |
|
||||
|
||||
---
|
||||
@ -73,7 +73,7 @@ wing/
|
||||
│ ├── audit/ 감사 로그
|
||||
│ ├── routes/ 레이어, 시뮬레이션
|
||||
│ ├── middleware/ 보안 (입력 살균, rate-limit)
|
||||
│ └── db/ DB 연결 (PostgreSQL, SQLite)
|
||||
│ └── db/ DB 연결 (PostgreSQL: wing, wing_auth)
|
||||
├── database/ SQL 초기화 스크립트
|
||||
├── docs/ 개발 문서
|
||||
├── .claude/ 팀 워크플로우 (rules, skills, scripts)
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user