feat: Phase 3 — API Explorer 지도 스캐폴딩 #34
273
frontend/package-lock.json
generated
273
frontend/package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "signal-batch-dashboard",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"maplibre-gl": "^5.18.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
@ -1022,6 +1023,115 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"get-stream": "^6.0.1",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"geojson-rewind": "geojson-rewind"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
|
||||
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/geojson-vt": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "24.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz",
|
||||
"integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/mlt": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.6.tgz",
|
||||
"integrity": "sha512-rgtY3x65lrrfXycLf6/T22ZnjTg5WgIOsptOIoCaMZy4O4UAKTyZlYY0h6v8le721pTptF94U65yMDQkug+URw==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.1.tgz",
|
||||
"integrity": "sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@maplibre/geojson-vt": "^5.0.4",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"pbf": "^4.0.1",
|
||||
"supercluster": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@ -1766,6 +1876,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@ -1803,6 +1919,15 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||
@ -2528,6 +2653,12 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.286",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
||||
@ -2928,6 +3059,24 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@ -3127,6 +3276,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@ -3140,6 +3295,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -3486,6 +3647,43 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.18.0.tgz",
|
||||
"integrity": "sha512-UtWxPBpHuFvEkM+5FVfcFG9ZKEWZQI6+PZkvLErr8Zs5ux+O7/KQ3JjSUvAfOlMeMgd/77qlHpOw0yHL7JU5cw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.7",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/geojson-vt": "^5.0.4",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.4.1",
|
||||
"@maplibre/mlt": "^1.1.6",
|
||||
"@maplibre/vt-pbf": "^4.2.1",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"earcut": "^3.0.2",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.1.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -3499,6 +3697,15 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"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/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -3506,6 +3713,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@ -3631,6 +3844,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -3680,6 +3905,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@ -3707,6 +3938,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -3717,6 +3954,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
@ -3865,6 +4108,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
@ -3910,6 +4162,12 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@ -3978,6 +4236,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@ -4035,6 +4302,12 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"maplibre-gl": "^5.18.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.0",
|
||||
|
||||
@ -9,6 +9,7 @@ const Dashboard = lazy(() => import('./pages/Dashboard.tsx'))
|
||||
const JobMonitor = lazy(() => import('./pages/JobMonitor.tsx'))
|
||||
const DataPipeline = lazy(() => import('./pages/DataPipeline.tsx'))
|
||||
const AreaStats = lazy(() => import('./pages/AreaStats.tsx'))
|
||||
const ApiExplorer = lazy(() => import('./pages/ApiExplorer.tsx'))
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_BASE_URL || '/signal-batch'
|
||||
|
||||
@ -51,7 +52,7 @@ export default function App() {
|
||||
<Route path="jobs" element={<Suspense fallback={<LoadingSpinner />}><JobMonitor /></Suspense>} />
|
||||
<Route path="pipeline" element={<Suspense fallback={<LoadingSpinner />}><DataPipeline /></Suspense>} />
|
||||
<Route path="area-stats" element={<Suspense fallback={<LoadingSpinner />}><AreaStats /></Suspense>} />
|
||||
{/* Phase 3+ 페이지 추가 예정 */}
|
||||
<Route path="api-explorer" element={<Suspense fallback={<LoadingSpinner />}><ApiExplorer /></Suspense>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
|
||||
47
frontend/src/api/gisApi.ts
Normal file
47
frontend/src/api/gisApi.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { fetchJson, postJson } from './httpClient.ts'
|
||||
|
||||
export interface HaeguBoundary {
|
||||
haegu_no: number
|
||||
haegu_name: string
|
||||
boundary_wkt: string
|
||||
center_lon: number
|
||||
center_lat: number
|
||||
}
|
||||
|
||||
export interface VesselTrackResult {
|
||||
mmsi: string
|
||||
nationalCode: string
|
||||
shipKindCode: string
|
||||
geometry: number[][]
|
||||
timestamps: number[]
|
||||
speeds: number[]
|
||||
}
|
||||
|
||||
export interface RecentPosition {
|
||||
mmsi: string
|
||||
lat: number
|
||||
lon: number
|
||||
sog: number
|
||||
cog: number
|
||||
lastUpdate: string
|
||||
vesselName?: string
|
||||
shipKindCode?: string
|
||||
}
|
||||
|
||||
export const gisApi = {
|
||||
getHaeguBoundaries(): Promise<HaeguBoundary[]> {
|
||||
return fetchJson('/api/v1/haegu/boundaries')
|
||||
},
|
||||
|
||||
getHaeguTracks(haeguNo: number, hours = 24): Promise<VesselTrackResult[]> {
|
||||
return fetchJson(`/api/v2/tracks/haegu/${haeguNo}?hours=${hours}`)
|
||||
},
|
||||
|
||||
getVesselTracks(mmsiList: string[], startTime: string, endTime: string): Promise<VesselTrackResult[]> {
|
||||
return postJson('/api/v2/tracks/vessels', { mmsiList, startTime, endTime })
|
||||
},
|
||||
|
||||
getRecentPositions(minutes = 10): Promise<RecentPosition[]> {
|
||||
return fetchJson(`/api/v1/vessels/recent-positions?minutes=${minutes}`)
|
||||
},
|
||||
}
|
||||
43
frontend/src/components/layout/Sidebar.tsx
Normal file
43
frontend/src/components/layout/Sidebar.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
|
||||
interface SidebarProps {
|
||||
children: ReactNode
|
||||
defaultOpen?: boolean
|
||||
width?: number
|
||||
}
|
||||
|
||||
export default function Sidebar({ children, defaultOpen = true, width = 320 }: SidebarProps) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 사이드바 패널 */}
|
||||
<div
|
||||
className="flex flex-col overflow-y-auto border-r border-border bg-surface transition-all duration-200"
|
||||
style={{ width: open ? width : 0, minWidth: open ? width : 0 }}
|
||||
>
|
||||
{open && <div className="flex-1 p-4">{children}</div>}
|
||||
</div>
|
||||
|
||||
{/* 토글 버튼 */}
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="absolute left-0 top-1/2 z-10 -translate-y-1/2 rounded-r-md border border-l-0 border-border bg-surface px-1 py-3 text-muted shadow-sm transition-all hover:bg-surface-hover hover:text-foreground"
|
||||
style={{ left: open ? width : 0 }}
|
||||
title={open ? 'Close sidebar' : 'Open sidebar'}
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
style={{ transform: open ? 'rotate(180deg)' : 'none' }}
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
71
frontend/src/components/map/MapContainer.tsx
Normal file
71
frontend/src/components/map/MapContainer.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useRef, useEffect, useState } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { useTheme } from '../../hooks/useTheme.ts'
|
||||
import {
|
||||
MAP_CENTER,
|
||||
MAP_DEFAULT_ZOOM,
|
||||
MAP_MIN_ZOOM,
|
||||
MAP_MAX_ZOOM,
|
||||
MAP_STYLE_LIGHT,
|
||||
MAP_STYLE_DARK,
|
||||
} from '../../utils/constants.ts'
|
||||
|
||||
interface MapContainerProps {
|
||||
onMapReady?: (map: maplibregl.Map) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function MapContainer({ onMapReady, className = '' }: MapContainerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const mapRef = useRef<maplibregl.Map | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: theme === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT,
|
||||
center: [MAP_CENTER.lng, MAP_CENTER.lat],
|
||||
zoom: MAP_DEFAULT_ZOOM,
|
||||
minZoom: MAP_MIN_ZOOM,
|
||||
maxZoom: MAP_MAX_ZOOM,
|
||||
attributionControl: false,
|
||||
})
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
map.addControl(
|
||||
new maplibregl.AttributionControl({ compact: true }),
|
||||
'bottom-right',
|
||||
)
|
||||
|
||||
map.on('load', () => {
|
||||
mapRef.current = map
|
||||
setReady(true)
|
||||
onMapReady?.(map)
|
||||
})
|
||||
|
||||
return () => {
|
||||
mapRef.current = null
|
||||
map.remove()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
/* 테마 변경 시 스타일 교체 */
|
||||
useEffect(() => {
|
||||
if (!mapRef.current || !ready) return
|
||||
const style = theme === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT
|
||||
mapRef.current.setStyle(style)
|
||||
}, [theme, ready])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`h-full w-full ${className}`}
|
||||
style={{ minHeight: 400 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -118,6 +118,19 @@ const en = {
|
||||
'area.stalePositions': 'Stale Positions',
|
||||
'area.checkedAt': 'Checked at',
|
||||
|
||||
// API Explorer
|
||||
'explorer.title': 'API Explorer',
|
||||
'explorer.apiType': 'API Type',
|
||||
'explorer.recentPositions': 'Recent Positions',
|
||||
'explorer.haeguTracks': 'Area Tracks',
|
||||
'explorer.vesselTracks': 'Vessel Tracks',
|
||||
'explorer.parameters': 'Parameters',
|
||||
'explorer.positionsDesc': 'Fetches vessels with position updates within 10 minutes.',
|
||||
'explorer.haeguDesc': 'Fetches vessel tracks within a specific area as GeoJSON.',
|
||||
'explorer.vesselDesc': 'Fetches tracks for specific vessels by MMSI list.',
|
||||
'explorer.comingSoon': 'Detailed API Demo (Coming Soon)',
|
||||
'explorer.comingSoonDesc': 'Request/Response panels, track layers, replay',
|
||||
|
||||
// Time Range
|
||||
'range.1d': '1D',
|
||||
'range.3d': '3D',
|
||||
|
||||
@ -118,6 +118,19 @@ const ko = {
|
||||
'area.stalePositions': '갱신 지연 위치',
|
||||
'area.checkedAt': '검증 시각',
|
||||
|
||||
// API Explorer
|
||||
'explorer.title': 'API 탐색기',
|
||||
'explorer.apiType': 'API 유형',
|
||||
'explorer.recentPositions': '최근 위치',
|
||||
'explorer.haeguTracks': '해구별 항적',
|
||||
'explorer.vesselTracks': '선박별 항적',
|
||||
'explorer.parameters': '파라미터',
|
||||
'explorer.positionsDesc': '최근 10분 이내 위치 업데이트된 선박 목록을 조회합니다.',
|
||||
'explorer.haeguDesc': '특정 해구 내 선박 항적을 GeoJSON 형태로 조회합니다.',
|
||||
'explorer.vesselDesc': 'MMSI 목록으로 특정 선박의 항적을 조회합니다.',
|
||||
'explorer.comingSoon': '상세 API 시연 (향후 구현)',
|
||||
'explorer.comingSoonDesc': 'Request/Response 패널, 항적 레이어, 리플레이',
|
||||
|
||||
// Time Range
|
||||
'range.1d': '1일',
|
||||
'range.3d': '3일',
|
||||
|
||||
105
frontend/src/pages/ApiExplorer.tsx
Normal file
105
frontend/src/pages/ApiExplorer.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useI18n } from '../hooks/useI18n.ts'
|
||||
import MapContainer from '../components/map/MapContainer.tsx'
|
||||
import Sidebar from '../components/layout/Sidebar.tsx'
|
||||
import type maplibregl from 'maplibre-gl'
|
||||
|
||||
type ApiMode = 'haegu' | 'vessel' | 'positions'
|
||||
|
||||
export default function ApiExplorer() {
|
||||
const { t } = useI18n()
|
||||
const [mode, setMode] = useState<ApiMode>('positions')
|
||||
const [, setMap] = useState<maplibregl.Map | null>(null)
|
||||
|
||||
const handleMapReady = useCallback((m: maplibregl.Map) => {
|
||||
setMap(m)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-3.5rem)] overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div className="relative flex">
|
||||
<Sidebar width={320}>
|
||||
<div className="space-y-4">
|
||||
{/* 제목 */}
|
||||
<h2 className="text-lg font-bold">{t('explorer.title')}</h2>
|
||||
|
||||
{/* API 유형 선택 */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-muted">
|
||||
{t('explorer.apiType')}
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
{([
|
||||
{ value: 'positions' as const, label: t('explorer.recentPositions') },
|
||||
{ value: 'haegu' as const, label: t('explorer.haeguTracks') },
|
||||
{ value: 'vessel' as const, label: t('explorer.vesselTracks') },
|
||||
] as const).map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setMode(opt.value)}
|
||||
className={`w-full rounded-md px-3 py-2 text-left text-sm transition ${
|
||||
mode === opt.value
|
||||
? 'bg-primary/10 font-medium text-primary'
|
||||
: 'text-foreground hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 파라미터 영역 */}
|
||||
<div className="rounded-lg border border-border p-3">
|
||||
<div className="mb-2 text-xs font-medium text-muted">
|
||||
{t('explorer.parameters')}
|
||||
</div>
|
||||
|
||||
{mode === 'positions' && (
|
||||
<div className="space-y-2 text-sm text-muted">
|
||||
<p>GET /api/v1/vessels/recent-positions</p>
|
||||
<p className="text-xs">{t('explorer.positionsDesc')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'haegu' && (
|
||||
<div className="space-y-2 text-sm text-muted">
|
||||
<p>GET /api/v2/tracks/haegu/:no</p>
|
||||
<p className="text-xs">{t('explorer.haeguDesc')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'vessel' && (
|
||||
<div className="space-y-2 text-sm text-muted">
|
||||
<p>POST /api/v2/tracks/vessels</p>
|
||||
<p className="text-xs">{t('explorer.vesselDesc')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 향후 구현 예정 안내 */}
|
||||
<div className="rounded-lg border border-dashed border-border p-3 text-center">
|
||||
<div className="text-xs text-muted">{t('explorer.comingSoon')}</div>
|
||||
<div className="mt-1 text-xs text-muted opacity-60">
|
||||
{t('explorer.comingSoonDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</div>
|
||||
|
||||
{/* Map area */}
|
||||
<div className="relative flex-1">
|
||||
<MapContainer onMapReady={handleMapReady} />
|
||||
|
||||
{/* 모드 표시 오버레이 */}
|
||||
<div className="absolute left-3 top-3 rounded-md bg-surface/90 px-3 py-1.5 text-xs font-medium shadow-sm backdrop-blur">
|
||||
{mode === 'positions' && t('explorer.recentPositions')}
|
||||
{mode === 'haegu' && t('explorer.haeguTracks')}
|
||||
{mode === 'vessel' && t('explorer.vesselTracks')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
frontend/src/utils/constants.ts
Normal file
25
frontend/src/utils/constants.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/** 지도 기본 설정 상수 */
|
||||
|
||||
/** 한반도 중심 좌표 */
|
||||
export const MAP_CENTER = { lng: 127.5, lat: 36.0 } as const
|
||||
|
||||
/** 기본 줌 레벨 */
|
||||
export const MAP_DEFAULT_ZOOM = 5.5
|
||||
|
||||
/** 지도 최소/최대 줌 */
|
||||
export const MAP_MIN_ZOOM = 2
|
||||
export const MAP_MAX_ZOOM = 18
|
||||
|
||||
/** 항적 색상 (선박 유형별) */
|
||||
export const TRACK_COLORS = {
|
||||
DEFAULT: '#3b82f6',
|
||||
CARGO: '#10b981',
|
||||
TANKER: '#f59e0b',
|
||||
PASSENGER: '#8b5cf6',
|
||||
FISHING: '#06b6d4',
|
||||
ABNORMAL: '#ef4444',
|
||||
} as const
|
||||
|
||||
/** OpenFreeMap 타일 URL (무료, 키 불필요) */
|
||||
export const MAP_STYLE_LIGHT = 'https://tiles.openfreemap.org/styles/liberty'
|
||||
export const MAP_STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark'
|
||||
불러오는 중...
Reference in New Issue
Block a user