diff --git a/CLAUDE.md b/CLAUDE.md index 29f527d..8695d31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/README.md b/README.md index 7832a3e..4fac801 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/backend/package-lock.json b/backend/package-lock.json index 761c796..a3d5138 100755 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 36aaee1..1e578cb 100755 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/db/database.ts b/backend/src/db/database.ts deleted file mode 100755 index de1109c..0000000 --- a/backend/src/db/database.ts +++ /dev/null @@ -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 diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 1a27bf1..1beff4f 100755 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -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) +}) diff --git a/backend/src/db/wingDb.ts b/backend/src/db/wingDb.ts new file mode 100644 index 0000000..83648a7 --- /dev/null +++ b/backend/src/db/wingDb.ts @@ -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 { + 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 } diff --git a/backend/src/routes/layers.ts b/backend/src/routes/layers.ts index ee50daf..d5ad1cb 100755 --- a/backend/src/routes/layers.ts +++ b/backend/src/routes/layers.ts @@ -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( + `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( + `SELECT ${LAYER_COLUMNS} FROM LAYER WHERE USE_YN = 'Y' ORDER BY LAYER_CD` + ) + const enrichedLayers = rows.map(enrichLayerWithMetadata) - const layerMap = new Map() + const layerMap = new Map() 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( + `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( + `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( + `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( + `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: '레이어 조회 실패' }) diff --git a/backend/src/server.ts b/backend/src/server.ts index 8d312c9..d46dbdc 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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 확장 대응) diff --git a/database/migration/001_layer_table.sql b/database/migration/001_layer_table.sql new file mode 100644 index 0000000..1d43b13 --- /dev/null +++ b/database/migration/001_layer_table.sql @@ -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); diff --git a/docs/COMMON-GUIDE.md b/docs/COMMON-GUIDE.md index e4f2b25..b77b5a9 100644 --- a/docs/COMMON-GUIDE.md +++ b/docs/COMMON-GUIDE.md @@ -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]) ``` --- diff --git a/docs/README.md b/docs/README.md index f077b30..1606965 100755 --- a/docs/README.md +++ b/docs/README.md @@ -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)