refactor(mpa): 탭 디렉토리를 MPA 컴포넌트 구조로 재편 #178
311
frontend/package-lock.json
generated
311
frontend/package-lock.json
generated
@ -1945,9 +1945,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
||||
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1959,9 +1959,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1973,9 +1973,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1987,9 +1987,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2001,9 +2001,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2015,9 +2015,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2029,9 +2029,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -2043,9 +2043,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
||||
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -2057,9 +2057,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2071,9 +2071,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2085,9 +2085,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -2099,9 +2099,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -2113,9 +2113,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -2127,9 +2127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -2141,9 +2141,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -2155,9 +2155,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -2169,9 +2169,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -2183,9 +2183,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2197,9 +2197,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
||||
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2211,9 +2211,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
||||
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2225,9 +2225,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
||||
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2239,9 +2239,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -2253,9 +2253,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -2267,9 +2267,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
||||
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2281,9 +2281,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
||||
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -2711,9 +2711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2721,13 +2721,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@ -2873,9 +2873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2927,9 +2927,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -3015,14 +3015,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
@ -3077,9 +3077,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -3946,9 +3946,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz",
|
||||
"integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==",
|
||||
"version": "4.5.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz",
|
||||
"integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -4055,16 +4055,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@ -4904,9 +4904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -4938,9 +4938,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@ -5169,9 +5169,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -5409,10 +5409,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
@ -5569,9 +5572,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -5633,9 +5636,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"version": "4.60.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -5649,31 +5652,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
||||
"@rollup/rollup-android-arm64": "4.60.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.1",
|
||||
"@rollup/rollup-darwin-x64": "4.60.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -5801,9 +5804,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
|
||||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
@ -6285,9 +6288,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -110,12 +110,20 @@ interface DispersionZone {
|
||||
angle: number;
|
||||
}
|
||||
|
||||
interface DispersionContour {
|
||||
level: string;
|
||||
threshold: number;
|
||||
color: string;
|
||||
segments: Array<[[number, number], [number, number]]>;
|
||||
}
|
||||
|
||||
interface DispersionResult {
|
||||
zones: DispersionZone[];
|
||||
timestamp: string;
|
||||
windDirection: number;
|
||||
substance: string;
|
||||
concentration: Record<string, string>;
|
||||
contours?: DispersionContour[];
|
||||
}
|
||||
|
||||
interface MapViewProps {
|
||||
@ -800,9 +808,9 @@ export function MapView({
|
||||
if (dispersionHeatmap && dispersionHeatmap.length > 0) {
|
||||
const maxConc = Math.max(...dispersionHeatmap.map((p) => p.concentration));
|
||||
const minConc = Math.min(
|
||||
...dispersionHeatmap.filter((p) => p.concentration > 0.01).map((p) => p.concentration),
|
||||
...dispersionHeatmap.filter((p) => p.concentration > 0.001).map((p) => p.concentration),
|
||||
);
|
||||
const filtered = dispersionHeatmap.filter((p) => p.concentration > 0.01);
|
||||
const filtered = dispersionHeatmap.filter((p) => p.concentration > 0.001);
|
||||
console.log(
|
||||
'[MapView] HNS 히트맵:',
|
||||
dispersionHeatmap.length,
|
||||
@ -870,7 +878,7 @@ export function MapView({
|
||||
|
||||
ctx.fillStyle = `rgba(${r},${g},${b},${a.toFixed(2)})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 6, 0, Math.PI * 2);
|
||||
ctx.arc(px, py, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
@ -890,11 +898,15 @@ export function MapView({
|
||||
|
||||
// --- HNS 대기확산 구역 (ScatterplotLayer, meters 단위) ---
|
||||
if (dispersionResult && incidentCoord) {
|
||||
// contour가 있으면 동심원 fill은 희미하게(contour가 실제 경계 표시), 없으면 진하게
|
||||
const hasContours = !!(dispersionResult.contours && dispersionResult.contours.length > 0);
|
||||
const zoneFillAlpha = hasContours ? 40 : 100;
|
||||
const zoneLineAlpha = hasContours ? 80 : 180;
|
||||
const zones = dispersionResult.zones.map((zone, idx) => ({
|
||||
position: [incidentCoord.lon, incidentCoord.lat] as [number, number],
|
||||
radius: zone.radius,
|
||||
fillColor: hexToRgba(zone.color, 100),
|
||||
lineColor: hexToRgba(zone.color, 180),
|
||||
fillColor: hexToRgba(zone.color, zoneFillAlpha),
|
||||
lineColor: hexToRgba(zone.color, zoneLineAlpha),
|
||||
level: zone.level,
|
||||
idx,
|
||||
}));
|
||||
@ -974,6 +986,27 @@ export function MapView({
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// --- HNS AEGL 등농도선 (PathLayer) ---
|
||||
if (dispersionResult.contours) {
|
||||
dispersionResult.contours.forEach((contour, cIdx) => {
|
||||
if (contour.segments.length === 0) return;
|
||||
const color = hexToRgba(contour.color, 230);
|
||||
result.push(
|
||||
new PathLayer({
|
||||
id: `hns-contour-${cIdx}-${contour.level}`,
|
||||
data: contour.segments,
|
||||
getPath: (d: [[number, number], [number, number]]) => d,
|
||||
getColor: color,
|
||||
getWidth: 3,
|
||||
widthUnits: 'pixels' as const,
|
||||
capRounded: true,
|
||||
jointRounded: true,
|
||||
pickable: false,
|
||||
}) as unknown as DeckLayer,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- 역추적 리플레이 ---
|
||||
|
||||
@ -325,7 +325,7 @@
|
||||
gap: 4px;
|
||||
padding: 5px 4px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-korean);
|
||||
cursor: pointer;
|
||||
@ -360,7 +360,7 @@
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
@ -386,7 +386,7 @@
|
||||
border: 1px solid rgba(6, 182, 212, 0.2);
|
||||
border-radius: 6px;
|
||||
color: var(--color-accent);
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
@ -411,7 +411,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.15s;
|
||||
background: rgba(15, 21, 36, 0.75);
|
||||
@ -450,7 +450,7 @@
|
||||
border-radius: 6px;
|
||||
padding: 5px 14px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 400;
|
||||
z-index: 20;
|
||||
@ -491,7 +491,7 @@
|
||||
}
|
||||
|
||||
.wii-value {
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-mono);
|
||||
@ -538,7 +538,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 1rem;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
@ -621,7 +621,7 @@
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
transform: translateX(-50%);
|
||||
font-size: 12px;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(0 0 4px rgba(245, 158, 11, 0.5));
|
||||
}
|
||||
@ -672,7 +672,7 @@
|
||||
}
|
||||
|
||||
.tlct {
|
||||
font-size: 14px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
@ -841,7 +841,7 @@
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 14px;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -1115,7 +1115,7 @@
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.15s;
|
||||
font-size: 12px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--fg-default);
|
||||
font-family: var(--font-korean);
|
||||
@ -1345,7 +1345,7 @@
|
||||
}
|
||||
|
||||
.lyr-ccustom label {
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-korean);
|
||||
}
|
||||
@ -1369,7 +1369,7 @@
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.lyr-style-label {
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-korean);
|
||||
@ -1411,7 +1411,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.lyr-style-val {
|
||||
font-size: 0.6875rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--fg-disabled);
|
||||
font-family: var(--font-mono);
|
||||
min-width: 28px;
|
||||
|
||||
@ -364,13 +364,13 @@ function fetchTasks(): Promise<DeidentifyTask[]> {
|
||||
function getStatusBadgeClass(status: TaskStatus): string {
|
||||
switch (status) {
|
||||
case '완료':
|
||||
return 'text-emerald-400 bg-emerald-500/10';
|
||||
return 'text-color-success bg-[rgba(34,197,94,0.1)]';
|
||||
case '진행중':
|
||||
return 'text-cyan-400 bg-cyan-500/10';
|
||||
return 'text-color-accent bg-[rgba(6,182,212,0.1)]';
|
||||
case '대기':
|
||||
return 'text-yellow-400 bg-yellow-500/10';
|
||||
return 'text-color-caution bg-[rgba(234,179,8,0.1)]';
|
||||
case '오류':
|
||||
return 'text-red-400 bg-red-500/10';
|
||||
return 'text-color-danger bg-[rgba(239,68,68,0.1)]';
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,7 +378,7 @@ function getStatusBadgeClass(status: TaskStatus): string {
|
||||
|
||||
function ProgressBar({ value }: { value: number }) {
|
||||
const colorClass =
|
||||
value === 100 ? 'bg-emerald-500' : value > 0 ? 'bg-cyan-500' : 'bg-bg-elevated';
|
||||
value === 100 ? 'bg-color-success' : value > 0 ? 'bg-color-accent' : 'bg-bg-elevated';
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 bg-bg-elevated rounded-full overflow-hidden">
|
||||
@ -405,13 +405,13 @@ interface TaskTableProps {
|
||||
function TaskTable({ rows, loading, onAction }: TaskTableProps) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{TABLE_HEADERS.map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -421,7 +421,7 @@ function TaskTable({ rows, loading, onAction }: TaskTableProps) {
|
||||
<tbody>
|
||||
{loading && rows.length === 0
|
||||
? Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i} className="border-b border-stroke-1 animate-pulse">
|
||||
<tr key={i} className="border-b border-stroke animate-pulse">
|
||||
{TABLE_HEADERS.map((_, j) => (
|
||||
<td key={j} className="px-3 py-2">
|
||||
<div className="h-3 bg-bg-elevated rounded w-14" />
|
||||
@ -430,7 +430,7 @@ function TaskTable({ rows, loading, onAction }: TaskTableProps) {
|
||||
</tr>
|
||||
))
|
||||
: rows.map((row) => (
|
||||
<tr key={row.id} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.id} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 text-t3 font-mono">{row.id}</td>
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.name}</td>
|
||||
<td
|
||||
@ -473,7 +473,7 @@ function TaskTable({ rows, loading, onAction }: TaskTableProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction('delete', row)}
|
||||
className="px-2 py-0.5 text-label-2 rounded bg-bg-elevated hover:bg-bg-card text-red-400 transition-colors whitespace-nowrap"
|
||||
className="px-2 py-0.5 text-label-2 rounded bg-bg-elevated hover:bg-bg-card text-color-danger transition-colors whitespace-nowrap"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
@ -499,7 +499,7 @@ const STEP_LABELS = ['소스선택', '데이터검증', '비식별화규칙', '
|
||||
|
||||
function StepIndicator({ current }: { current: number }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-0 px-6 py-4 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-center gap-0 px-6 py-4 border-b border-stroke">
|
||||
{STEP_LABELS.map((label, i) => {
|
||||
const stepNum = i + 1;
|
||||
const isDone = stepNum < current;
|
||||
@ -508,11 +508,11 @@ function StepIndicator({ current }: { current: number }) {
|
||||
<div key={label} className="flex items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<div
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-semibold transition-colors ${
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center text-caption font-semibold transition-colors ${
|
||||
isDone
|
||||
? 'bg-emerald-500 text-white'
|
||||
? 'bg-color-success text-white'
|
||||
: isActive
|
||||
? 'bg-cyan-500 text-white'
|
||||
? 'bg-color-accent text-white'
|
||||
: 'bg-bg-elevated text-t3'
|
||||
}`}
|
||||
>
|
||||
@ -531,7 +531,7 @@ function StepIndicator({ current }: { current: number }) {
|
||||
</div>
|
||||
<span
|
||||
className={`mt-1 text-label-2 whitespace-nowrap ${
|
||||
isActive ? 'text-cyan-400' : isDone ? 'text-emerald-400' : 'text-t3'
|
||||
isActive ? 'text-color-accent' : isDone ? 'text-color-success' : 'text-t3'
|
||||
}`}
|
||||
>
|
||||
{stepNum}.{label}
|
||||
@ -539,7 +539,7 @@ function StepIndicator({ current }: { current: number }) {
|
||||
</div>
|
||||
{i < STEP_LABELS.length - 1 && (
|
||||
<div
|
||||
className={`w-10 h-px mx-1 mb-4 ${i + 1 < current ? 'bg-emerald-500' : 'bg-stroke-1'}`}
|
||||
className={`w-10 h-px mx-1 mb-4 ${i + 1 < current ? 'bg-color-success' : 'bg-stroke-1'}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -567,18 +567,18 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-t2 mb-1">작업명 *</label>
|
||||
<label className="block text-caption font-medium text-t2 mb-1">작업명 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={wizard.taskName}
|
||||
onChange={(e) => onChange({ taskName: e.target.value })}
|
||||
placeholder="작업 이름을 입력하세요"
|
||||
className="w-full px-3 py-2 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500"
|
||||
className="w-full px-3 py-2 text-caption rounded bg-bg-elevated border border-stroke text-t1 placeholder:text-t3 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-t2 mb-2">소스 유형 *</label>
|
||||
<label className="block text-caption font-medium text-t2 mb-2">소스 유형 *</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{(
|
||||
[
|
||||
@ -596,14 +596,14 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
onChange={() => onChange({ sourceType: val })}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t1">{label}</span>
|
||||
<span className="text-caption text-t1">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{wizard.sourceType === 'db' && (
|
||||
<div className="grid grid-cols-2 gap-3 p-4 rounded bg-bg-surface border border-stroke-1">
|
||||
<div className="grid grid-cols-2 gap-3 p-4 rounded bg-bg-surface border border-stroke">
|
||||
{(
|
||||
[
|
||||
['host', '호스트', 'localhost'],
|
||||
@ -619,7 +619,7 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
value={wizard.dbConfig[key]}
|
||||
onChange={(e) => handleDbChange(key, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500"
|
||||
className="w-full px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 placeholder:text-t3 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -627,7 +627,7 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
)}
|
||||
|
||||
{wizard.sourceType === 'file' && (
|
||||
<div className="p-8 rounded border-2 border-dashed border-stroke-1 bg-bg-surface flex flex-col items-center gap-2 text-center">
|
||||
<div className="p-8 rounded border-2 border-dashed border-stroke bg-bg-surface flex flex-col items-center gap-2 text-center">
|
||||
<svg className="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
@ -636,13 +636,13 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-xs text-t2">파일을 드래그하거나 클릭하여 업로드</p>
|
||||
<p className="text-caption text-t2">파일을 드래그하거나 클릭하여 업로드</p>
|
||||
<p className="text-label-2 text-t3">CSV, XLSX, JSON 지원 (최대 500MB)</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{wizard.sourceType === 'api' && (
|
||||
<div className="p-4 rounded bg-bg-surface border border-stroke-1 space-y-3">
|
||||
<div className="p-4 rounded bg-bg-surface border border-stroke space-y-3">
|
||||
<div>
|
||||
<label className="block text-label-2 text-t3 mb-1">API URL</label>
|
||||
<input
|
||||
@ -650,7 +650,7 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
value={wizard.apiConfig.url}
|
||||
onChange={(e) => handleApiChange('url', e.target.value)}
|
||||
placeholder="https://api.example.com/data"
|
||||
className="w-full px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500"
|
||||
className="w-full px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 placeholder:text-t3 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -658,7 +658,7 @@ function Step1({ wizard, onChange }: Step1Props) {
|
||||
<select
|
||||
value={wizard.apiConfig.method}
|
||||
onChange={(e) => handleApiChange('method', e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
@ -688,23 +688,23 @@ function Step2({ wizard, onChange }: Step2Props) {
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ label: '총 데이터 건수', value: '15,240건', color: 'text-t1' },
|
||||
{ label: '중복', value: '0건', color: 'text-emerald-400' },
|
||||
{ label: '누락값', value: '23건', color: 'text-yellow-400' },
|
||||
{ label: '중복', value: '0건', color: 'text-color-success' },
|
||||
{ label: '누락값', value: '23건', color: 'text-color-caution' },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className="p-3 rounded bg-bg-surface border border-stroke-1">
|
||||
<div key={stat.label} className="p-3 rounded bg-bg-surface border border-stroke">
|
||||
<p className="text-label-2 text-t3 mb-1">{stat.label}</p>
|
||||
<p className={`text-sm font-semibold ${stat.color}`}>{stat.value}</p>
|
||||
<p className={`text-body-2 font-semibold ${stat.color}`}>{stat.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-t2 mb-2">스키마 분석 결과 — 포함 필드 선택</h4>
|
||||
<div className="rounded border border-stroke-1 overflow-hidden">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<h4 className="text-caption font-medium text-t2 mb-2">스키마 분석 결과 — 포함 필드 선택</h4>
|
||||
<div className="rounded border border-stroke overflow-hidden">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1 w-8">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke w-8">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wizard.fields.every((f) => f.selected)}
|
||||
@ -716,15 +716,15 @@ function Step2({ wizard, onChange }: Step2Props) {
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">필드명</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">필드명</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">
|
||||
데이터 타입
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{wizard.fields.map((field, idx) => (
|
||||
<tr key={field.name} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={field.name} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -765,32 +765,32 @@ function Step3({ wizard, onChange }: Step3Props) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-stroke-1 overflow-hidden">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<div className="rounded border border-stroke overflow-hidden">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">필드명</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">필드명</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">
|
||||
데이터타입
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">
|
||||
선택된 기법
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke-1">설정값</th>
|
||||
<th className="px-3 py-2 text-left font-medium border-b border-stroke">설정값</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedFields.map((field) => {
|
||||
const globalIdx = wizard.fields.findIndex((f) => f.name === field.name);
|
||||
return (
|
||||
<tr key={field.name} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={field.name} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1">{field.name}</td>
|
||||
<td className="px-3 py-2 text-t3">{field.dataType}</td>
|
||||
<td className="px-3 py-2">
|
||||
<select
|
||||
value={field.technique}
|
||||
onChange={(e) => updateField(globalIdx, 'technique', e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{TECHNIQUES.map((t) => (
|
||||
<option key={t} value={t}>
|
||||
@ -804,7 +804,7 @@ function Step3({ wizard, onChange }: Step3Props) {
|
||||
type="text"
|
||||
value={field.configValue}
|
||||
onChange={(e) => updateField(globalIdx, 'configValue', e.target.value)}
|
||||
className="w-full px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="w-full px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@ -822,15 +822,15 @@ function Step3({ wizard, onChange }: Step3Props) {
|
||||
onChange={(e) => onChange({ saveAsTemplate: e.target.checked })}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t2">템플릿으로 저장</span>
|
||||
<span className="text-caption text-t2">템플릿으로 저장</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-t3">이전 템플릿 적용:</span>
|
||||
<span className="text-caption text-t3">이전 템플릿 적용:</span>
|
||||
<select
|
||||
value={wizard.applyTemplate}
|
||||
onChange={(e) => onChange({ applyTemplate: e.target.value })}
|
||||
className="px-2.5 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
<option value="">선택 안 함</option>
|
||||
{TEMPLATES.map((t) => (
|
||||
@ -881,19 +881,19 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
className="mt-0.5 accent-cyan-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-t1">{label}</span>
|
||||
<span className="text-caption font-medium text-t1">{label}</span>
|
||||
<p className="text-label-2 text-t3 mt-0.5">{desc}</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{val === 'scheduled' && wizard.processMode === 'scheduled' && (
|
||||
<div className="mt-3 ml-5 p-4 rounded bg-bg-surface border border-stroke-1 space-y-3">
|
||||
<div className="mt-3 ml-5 p-4 rounded bg-bg-surface border border-stroke space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs text-t3 w-16 shrink-0">실행 시간</label>
|
||||
<label className="text-caption text-t3 w-16 shrink-0">실행 시간</label>
|
||||
<select
|
||||
value={wizard.scheduleConfig.hour}
|
||||
onChange={(e) => handleScheduleChange('hour', e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{HOURS.map((h) => (
|
||||
<option key={h} value={h}>
|
||||
@ -903,7 +903,7 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<label className="text-xs text-t3 w-16 shrink-0 mt-1">반복</label>
|
||||
<label className="text-caption text-t3 w-16 shrink-0 mt-1">반복</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{(
|
||||
[
|
||||
@ -921,12 +921,12 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
onChange={() => handleScheduleChange('repeatType', rt)}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t1">{rl}</span>
|
||||
<span className="text-caption text-t1">{rl}</span>
|
||||
{rt === 'weekly' && wizard.scheduleConfig.repeatType === 'weekly' && (
|
||||
<select
|
||||
value={wizard.scheduleConfig.weekday}
|
||||
onChange={(e) => handleScheduleChange('weekday', e.target.value)}
|
||||
className="ml-1 px-2 py-0.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="ml-1 px-2 py-0.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{WEEKDAYS.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
@ -940,12 +940,12 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs text-t3 w-16 shrink-0">시작일</label>
|
||||
<label className="text-caption text-t3 w-16 shrink-0">시작일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={wizard.scheduleConfig.startDate}
|
||||
onChange={(e) => handleScheduleChange('startDate', e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 mt-1">
|
||||
@ -956,7 +956,7 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
onChange={(e) => handleScheduleChange('notifyOnComplete', e.target.checked)}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t2">처리 완료 시 이메일 알림</span>
|
||||
<span className="text-caption text-t2">처리 완료 시 이메일 알림</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@ -965,29 +965,29 @@ function Step4({ wizard, onChange }: Step4Props) {
|
||||
onChange={(e) => handleScheduleChange('notifyOnError', e.target.checked)}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t2">오류 발생 시 즉시 알림</span>
|
||||
<span className="text-caption text-t2">오류 발생 시 즉시 알림</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{val === 'oneshot' && wizard.processMode === 'oneshot' && (
|
||||
<div className="mt-3 ml-5 p-4 rounded bg-bg-surface border border-stroke-1 space-y-3">
|
||||
<div className="mt-3 ml-5 p-4 rounded bg-bg-surface border border-stroke space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs text-t3 w-16 shrink-0">실행 날짜</label>
|
||||
<label className="text-caption text-t3 w-16 shrink-0">실행 날짜</label>
|
||||
<input
|
||||
type="date"
|
||||
value={wizard.oneshotConfig.date}
|
||||
onChange={(e) => handleOneshotChange('date', e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs text-t3 w-16 shrink-0">실행 시간</label>
|
||||
<label className="text-caption text-t3 w-16 shrink-0">실행 시간</label>
|
||||
<select
|
||||
value={wizard.oneshotConfig.hour}
|
||||
onChange={(e) => handleOneshotChange('hour', e.target.value)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{HOURS.map((h) => (
|
||||
<option key={h} value={h}>
|
||||
@ -1042,11 +1042,11 @@ function Step5({ wizard, onChange }: Step5Props) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-stroke-1 overflow-hidden">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<div className="rounded border border-stroke overflow-hidden">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<tbody>
|
||||
{summaryRows.map(({ label, value }) => (
|
||||
<tr key={label} className="border-b border-stroke-1 last:border-b-0">
|
||||
<tr key={label} className="border-b border-stroke last:border-b-0">
|
||||
<td className="px-4 py-2.5 text-t3 bg-bg-elevated w-36 font-medium">{label}</td>
|
||||
<td className="px-4 py-2.5 text-t1">{value}</td>
|
||||
</tr>
|
||||
@ -1055,14 +1055,14 @@ function Step5({ wizard, onChange }: Step5Props) {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer p-3 rounded border border-stroke-1 bg-bg-surface">
|
||||
<label className="flex items-center gap-2 cursor-pointer p-3 rounded border border-stroke bg-bg-surface">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wizard.confirmed}
|
||||
onChange={(e) => onChange({ confirmed: e.target.checked })}
|
||||
className="accent-cyan-500"
|
||||
/>
|
||||
<span className="text-xs text-t1 font-medium">위 정보를 확인했습니다.</span>
|
||||
<span className="text-caption text-t1 font-medium">위 정보를 확인했습니다.</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@ -1097,13 +1097,13 @@ const INITIAL_WIZARD: WizardState = {
|
||||
function getAuditResultClass(type: AuditLogEntry['resultType']): string {
|
||||
switch (type) {
|
||||
case '성공':
|
||||
return 'text-emerald-400 bg-emerald-500/10';
|
||||
return 'text-color-success bg-[rgba(34,197,94,0.1)]';
|
||||
case '진행중':
|
||||
return 'text-cyan-400 bg-cyan-500/10';
|
||||
return 'text-color-accent bg-[rgba(6,182,212,0.1)]';
|
||||
case '실패':
|
||||
return 'text-red-400 bg-red-500/10';
|
||||
return 'text-color-danger bg-[rgba(239,68,68,0.1)]';
|
||||
case '거부':
|
||||
return 'text-yellow-400 bg-yellow-500/10';
|
||||
return 'text-color-caution bg-[rgba(234,179,8,0.1)]';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1127,10 +1127,10 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div className="bg-bg-card border border-stroke-1 rounded-lg shadow-2xl w-full max-w-5xl max-h-[85vh] flex flex-col">
|
||||
<div className="bg-bg-card border border-stroke rounded-lg shadow-2xl w-full max-w-5xl max-h-[85vh] flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h3 className="text-sm font-semibold text-t1">감시 감독 (감사로그) — {task.name}</h3>
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke shrink-0">
|
||||
<h3 className="text-body-2 font-semibold text-t1">감시 감독 (감사로그) — {task.name}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-t3 hover:text-t1 transition-colors text-lg leading-none"
|
||||
@ -1140,26 +1140,26 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
</div>
|
||||
|
||||
{/* 필터 바 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-stroke-1 shrink-0 bg-bg-base">
|
||||
<span className="text-xs text-t3">기간:</span>
|
||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-stroke shrink-0 bg-bg-base">
|
||||
<span className="text-caption text-t3">기간:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
<span className="text-xs text-t3">~</span>
|
||||
<span className="text-caption text-t3">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
/>
|
||||
<span className="text-xs text-t3 ml-2">작업자:</span>
|
||||
<span className="text-caption text-t3 ml-2">작업자:</span>
|
||||
<select
|
||||
value={filterOperator}
|
||||
onChange={(e) => setFilterOperator(e.target.value)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{operators.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
@ -1171,13 +1171,13 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
|
||||
{/* 로그 테이블 */}
|
||||
<div className="flex-1 overflow-auto px-5 py-3">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['시간', '작업자', '작업', '대상 데이터', '결과', '상세'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -1195,7 +1195,7 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
filteredLogs.map((log) => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className={`border-b border-stroke-1 hover:bg-bg-surface/50 cursor-pointer ${selectedLog?.id === log.id ? 'bg-bg-surface/70' : ''}`}
|
||||
className={`border-b border-stroke hover:bg-bg-surface/50 cursor-pointer ${selectedLog?.id === log.id ? 'bg-bg-surface/70' : ''}`}
|
||||
onClick={() => setSelectedLog(log)}
|
||||
>
|
||||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">
|
||||
@ -1219,7 +1219,7 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
e.stopPropagation();
|
||||
setSelectedLog(log);
|
||||
}}
|
||||
className="px-2 py-0.5 text-label-2 rounded bg-bg-elevated hover:bg-bg-card text-cyan-400 transition-colors whitespace-nowrap"
|
||||
className="px-2 py-0.5 text-label-2 rounded bg-bg-elevated hover:bg-bg-card text-color-accent transition-colors whitespace-nowrap"
|
||||
>
|
||||
보기
|
||||
</button>
|
||||
@ -1233,9 +1233,9 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
|
||||
{/* 로그 상세 정보 */}
|
||||
{selectedLog && (
|
||||
<div className="px-5 py-3 border-t border-stroke-1 shrink-0 bg-bg-base">
|
||||
<h4 className="text-xs font-semibold text-t1 mb-2">로그 상세 정보</h4>
|
||||
<div className="bg-bg-elevated border border-stroke-1 rounded p-3 text-xs grid grid-cols-2 gap-x-6 gap-y-1.5">
|
||||
<div className="px-5 py-3 border-t border-stroke shrink-0 bg-bg-base">
|
||||
<h4 className="text-caption font-semibold text-t1 mb-2">로그 상세 정보</h4>
|
||||
<div className="bg-bg-elevated border border-stroke rounded p-3 text-caption grid grid-cols-2 gap-x-6 gap-y-1.5">
|
||||
<div>
|
||||
<span className="text-t3">로그ID:</span>{' '}
|
||||
<span className="text-t1 font-mono">{selectedLog.id}</span>
|
||||
@ -1284,16 +1284,16 @@ function AuditLogModal({ task, onClose }: AuditLogModalProps) {
|
||||
)}
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-stroke-1 shrink-0">
|
||||
<button className="px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors">
|
||||
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-stroke shrink-0">
|
||||
<button className="px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors">
|
||||
상세내용 다운로드 (암호화됨)
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors">
|
||||
<button className="px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors">
|
||||
기간별 보고서 생성
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors"
|
||||
className="px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
@ -1337,10 +1337,10 @@ function WizardModal({ onClose, onSubmit }: WizardModalProps) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div className="bg-bg-card border border-stroke-1 rounded-lg shadow-2xl w-full max-w-4xl mx-4 flex flex-col max-h-[90vh]">
|
||||
<div className="bg-bg-card border border-stroke rounded-lg shadow-2xl w-full max-w-4xl mx-4 flex flex-col max-h-[90vh]">
|
||||
{/* 모달 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke-1 shrink-0">
|
||||
<h3 className="text-sm font-semibold text-t1">새 비식별화 작업</h3>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke shrink-0">
|
||||
<h3 className="text-body-2 font-semibold text-t1">새 비식별화 작업</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded text-t3 hover:text-t1 hover:bg-bg-elevated transition-colors"
|
||||
@ -1369,18 +1369,18 @@ function WizardModal({ onClose, onSubmit }: WizardModalProps) {
|
||||
</div>
|
||||
|
||||
{/* 푸터 버튼 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-stroke-1 shrink-0">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-stroke shrink-0">
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
disabled={wizard.step === 1}
|
||||
className="px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors"
|
||||
className="px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
@ -1388,7 +1388,7 @@ function WizardModal({ onClose, onSubmit }: WizardModalProps) {
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
className="px-3 py-1.5 text-xs rounded bg-cyan-600 hover:bg-cyan-700 text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-caption rounded bg-color-accent hover:bg-color-accent-muted text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
@ -1396,7 +1396,7 @@ function WizardModal({ onClose, onSubmit }: WizardModalProps) {
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canProceed()}
|
||||
className="px-3 py-1.5 text-xs rounded bg-cyan-600 hover:bg-cyan-700 text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-3 py-1.5 text-caption rounded bg-color-accent hover:bg-color-accent-muted text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
처리 시작
|
||||
</button>
|
||||
@ -1489,11 +1489,11 @@ export default function DeidentifyPanel() {
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">비식별화조치</h2>
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke shrink-0">
|
||||
<h2 className="text-body-2 font-semibold text-t1">비식별화조치</h2>
|
||||
<button
|
||||
onClick={() => setShowWizard(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-cyan-600 hover:bg-cyan-700 text-white transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-color-accent hover:bg-color-accent-muted text-white transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
@ -1503,37 +1503,37 @@ export default function DeidentifyPanel() {
|
||||
</div>
|
||||
|
||||
{/* 상태 요약 */}
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke-1 bg-bg-base">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-emerald-500/10 text-emerald-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<div className="flex items-center gap-3 px-5 py-2 shrink-0 border-b border-stroke bg-bg-base">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-[rgba(34,197,94,0.1)] text-color-success">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-color-success" />
|
||||
완료 {completedCount}건
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-cyan-500/10 text-cyan-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-cyan-400" />
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-[rgba(6,182,212,0.1)] text-color-accent">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-color-accent" />
|
||||
진행중 {inProgressCount}건
|
||||
</span>
|
||||
{errorCount > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-red-500/10 text-red-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-400" />
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-caption bg-[rgba(239,68,68,0.1)] text-color-danger">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-color-danger" />
|
||||
오류 {errorCount}건
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-t3">전체 {tasks.length}건</span>
|
||||
<span className="text-caption text-t3">전체 {tasks.length}건</span>
|
||||
</div>
|
||||
|
||||
{/* 검색/필터 */}
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 shrink-0 border-b border-stroke-1">
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 shrink-0 border-b border-stroke">
|
||||
<input
|
||||
type="text"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
placeholder="작업명 검색"
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 placeholder:text-t3 focus:outline-none focus:border-cyan-500 w-40"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 placeholder:text-t3 focus:outline-none focus:border-color-accent w-40"
|
||||
/>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value as FilterStatus)}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
{(['모두', '완료', '진행중', '대기', '오류'] as FilterStatus[]).map((s) => (
|
||||
<option key={s} value={s}>
|
||||
@ -1544,7 +1544,7 @@ export default function DeidentifyPanel() {
|
||||
<select
|
||||
value={filterPeriod}
|
||||
onChange={(e) => setFilterPeriod(e.target.value as '7' | '30' | '90')}
|
||||
className="px-2.5 py-1.5 text-xs rounded bg-bg-elevated border border-stroke-1 text-t1 focus:outline-none focus:border-cyan-500"
|
||||
className="px-2.5 py-1.5 text-caption rounded bg-bg-elevated border border-stroke text-t1 focus:outline-none focus:border-color-accent"
|
||||
>
|
||||
<option value="7">최근 7일</option>
|
||||
<option value="30">최근 30일</option>
|
||||
|
||||
@ -153,8 +153,8 @@ function PermCell({ state, onToggle, label, readOnly = false }: PermCellProps) {
|
||||
break;
|
||||
case 'explicit-denied':
|
||||
classes = readOnly
|
||||
? `${baseClasses} bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-red-400 cursor-default`
|
||||
: `${baseClasses} bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-red-400 cursor-pointer hover:border-red-400`;
|
||||
? `${baseClasses} bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-color-danger cursor-default`
|
||||
: `${baseClasses} bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-color-danger cursor-pointer hover:border-color-danger`;
|
||||
icon = '—';
|
||||
break;
|
||||
case 'forced-denied':
|
||||
@ -219,7 +219,7 @@ function TreeRow({
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
||||
<tr className="border-b border-stroke hover:bg-[rgba(6,182,212,0.04)] transition-colors">
|
||||
<td className="px-3 py-1">
|
||||
<div className="flex items-center" style={{ paddingLeft: indent }}>
|
||||
{hasChildren ? (
|
||||
@ -311,7 +311,7 @@ function PermLegend() {
|
||||
상속
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="inline-block w-3 h-3 rounded border bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-red-400 text-center text-caption leading-3">
|
||||
<span className="inline-block w-3 h-3 rounded border bg-[rgba(239,68,68,0.08)] border-[rgba(239,68,68,0.3)] text-color-danger text-center text-caption leading-3">
|
||||
—
|
||||
</span>
|
||||
거부
|
||||
@ -443,8 +443,7 @@ function RolePermTab({
|
||||
className="flex items-center gap-1.5 px-4 py-2 border-b border-stroke bg-bg-surface overflow-x-auto"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{roles.map((role, idx) => {
|
||||
const color = getRoleColor(role.code, idx);
|
||||
{roles.map((role) => {
|
||||
const isSelected = selectedRoleSn === role.sn;
|
||||
return (
|
||||
<div key={role.sn} className="flex items-center gap-0.5 flex-shrink-0">
|
||||
@ -452,10 +451,9 @@ function RolePermTab({
|
||||
onClick={() => setSelectedRoleSn(role.sn)}
|
||||
className={`px-2.5 py-1 text-label-2 font-semibold rounded-md transition-all font-korean ${
|
||||
isSelected
|
||||
? 'border-2 shadow-[0_0_8px_rgba(6,182,212,0.2)]'
|
||||
: 'border border-stroke text-fg-disabled hover:border-stroke'
|
||||
? 'border-2 border-color-accent text-color-accent shadow-[0_0_8px_rgba(6,182,212,0.2)]'
|
||||
: 'border border-stroke text-fg-disabled hover:border-stroke-light hover:text-fg-sub'
|
||||
}`}
|
||||
style={isSelected ? { borderColor: color, color } : undefined}
|
||||
>
|
||||
{editingRoleSn === role.sn ? (
|
||||
<input
|
||||
@ -495,7 +493,7 @@ function RolePermTab({
|
||||
{role.code !== 'ADMIN' && (
|
||||
<button
|
||||
onClick={() => handleDeleteRole(role.sn, role.name)}
|
||||
className="w-5 h-5 flex items-center justify-center text-fg-disabled hover:text-red-400 transition-colors"
|
||||
className="w-5 h-5 flex items-center justify-center text-fg-disabled hover:text-color-danger transition-colors"
|
||||
title="역할 삭제"
|
||||
>
|
||||
<svg
|
||||
@ -612,7 +610,7 @@ function RolePermTab({
|
||||
/>
|
||||
</div>
|
||||
{createError && (
|
||||
<div className="px-3 py-2 text-label-2 text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)] rounded-md font-korean">
|
||||
<div className="px-3 py-2 text-label-2 text-color-danger bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)] rounded-md font-korean">
|
||||
{createError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -297,7 +297,7 @@ function PipelineCard({ node }: { node: PipelineNode }) {
|
||||
<div
|
||||
className={`flex-1 min-w-0 bg-bg-card rounded border border-stroke-1 border-l-2 ${borderStyle} p-3 flex flex-col gap-1.5`}
|
||||
>
|
||||
<div className="text-xs font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<div className="text-caption font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<span
|
||||
className={`self-start inline-block px-1.5 py-0.5 rounded text-label-2 font-medium ${badgeStyle}`}
|
||||
>
|
||||
@ -316,7 +316,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<div className="flex-1 h-16 bg-bg-elevated rounded w-28" />
|
||||
{i < 4 && <span className="text-t3 text-sm px-0.5">→</span>}
|
||||
{i < 4 && <span className="text-t3 text-body-2 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -328,7 +328,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{nodes.map((node, idx) => (
|
||||
<div key={node.id} className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<PipelineCard node={node} />
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-sm shrink-0 px-0.5">→</span>}
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-body-2 shrink-0 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -367,7 +367,7 @@ const LOG_HEADERS = ['시간', '데이터소스', '데이터종류', '크기', '
|
||||
function DataLogTable({ rows, loading }: { rows: DataLogRow[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{LOG_HEADERS.map((h) => (
|
||||
@ -440,7 +440,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
}
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return <p className="text-xs text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
return <p className="text-caption text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -448,7 +448,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-xs ${getAlertStyle(alert.level)}`}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-caption ${getAlertStyle(alert.level)}`}
|
||||
>
|
||||
<span className="font-semibold shrink-0">[{alert.level}]</span>
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
@ -509,10 +509,10 @@ export default function RndHnsAtmosPanel() {
|
||||
{/* ── 헤더 ── */}
|
||||
<div className="shrink-0 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<h2 className="text-sm font-semibold text-t1">HNS 대기확산 (충북대) 연계 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">HNS 대기확산 (충북대) 연계 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -524,7 +524,7 @@ export default function RndHnsAtmosPanel() {
|
||||
<button
|
||||
onClick={() => void fetchData()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -544,7 +544,7 @@ export default function RndHnsAtmosPanel() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 요약 통계 바 */}
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-xs text-t3 border-t border-stroke-1">
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-caption text-t3 border-t border-stroke-1">
|
||||
<span>
|
||||
정상 수신: <span className="text-emerald-400 font-medium">{totalReceived}건</span>
|
||||
</span>
|
||||
@ -567,7 +567,7 @@ export default function RndHnsAtmosPanel() {
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 파이프라인 현황 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<h3 className="text-xs font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
<h3 className="text-caption font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
데이터 파이프라인 현황
|
||||
</h3>
|
||||
<PipelineFlow nodes={pipeline} loading={loading} />
|
||||
@ -576,7 +576,7 @@ export default function RndHnsAtmosPanel() {
|
||||
{/* 필터 바 + 수신 이력 테이블 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between mb-3 gap-3 flex-wrap">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
데이터 수신 이력
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@ -584,7 +584,7 @@ export default function RndHnsAtmosPanel() {
|
||||
<select
|
||||
value={filterSource}
|
||||
onChange={(e) => setFilterSource(e.target.value as FilterSource)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (소스)</option>
|
||||
<option value="HYCOM">HYCOM</option>
|
||||
@ -595,7 +595,7 @@ export default function RndHnsAtmosPanel() {
|
||||
<select
|
||||
value={filterReceive}
|
||||
onChange={(e) => setFilterReceive(e.target.value as FilterReceive)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (상태)</option>
|
||||
<option value="수신완료">수신완료</option>
|
||||
@ -607,13 +607,13 @@ export default function RndHnsAtmosPanel() {
|
||||
<select
|
||||
value={filterPeriod}
|
||||
onChange={(e) => setFilterPeriod(e.target.value as FilterPeriod)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="6h">최근 6시간</option>
|
||||
<option value="12h">최근 12시간</option>
|
||||
<option value="24h">최근 24시간</option>
|
||||
</select>
|
||||
<span className="text-xs text-t3">{filteredLogs.length}건</span>
|
||||
<span className="text-caption text-t3">{filteredLogs.length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<DataLogTable rows={filteredLogs} loading={loading} />
|
||||
@ -621,7 +621,7 @@ export default function RndHnsAtmosPanel() {
|
||||
|
||||
{/* 알림 현황 */}
|
||||
<section className="px-5 pt-4 pb-5">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<AlertList alerts={alerts} loading={loading} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -297,7 +297,7 @@ function PipelineCard({ node }: { node: PipelineNode }) {
|
||||
<div
|
||||
className={`flex-1 min-w-0 bg-bg-card rounded border border-stroke-1 border-l-2 ${borderStyle} p-3 flex flex-col gap-1.5`}
|
||||
>
|
||||
<div className="text-xs font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<div className="text-caption font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<span
|
||||
className={`self-start inline-block px-1.5 py-0.5 rounded text-label-2 font-medium ${badgeStyle}`}
|
||||
>
|
||||
@ -316,7 +316,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<div className="flex-1 h-16 bg-bg-elevated rounded w-28" />
|
||||
{i < 4 && <span className="text-t3 text-sm px-0.5">→</span>}
|
||||
{i < 4 && <span className="text-t3 text-body-2 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -328,7 +328,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{nodes.map((node, idx) => (
|
||||
<div key={node.id} className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<PipelineCard node={node} />
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-sm shrink-0 px-0.5">→</span>}
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-body-2 shrink-0 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -367,7 +367,7 @@ const LOG_HEADERS = ['시간', '데이터소스', '데이터종류', '크기', '
|
||||
function DataLogTable({ rows, loading }: { rows: DataLogRow[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{LOG_HEADERS.map((h) => (
|
||||
@ -440,7 +440,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
}
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return <p className="text-xs text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
return <p className="text-caption text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -448,7 +448,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-xs ${getAlertStyle(alert.level)}`}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-caption ${getAlertStyle(alert.level)}`}
|
||||
>
|
||||
<span className="font-semibold shrink-0">[{alert.level}]</span>
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
@ -509,10 +509,10 @@ export default function RndKospsPanel() {
|
||||
{/* ── 헤더 ── */}
|
||||
<div className="shrink-0 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<h2 className="text-sm font-semibold text-t1">유출유확산예측 (KOSPS) 연계 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">유출유확산예측 (KOSPS) 연계 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -524,7 +524,7 @@ export default function RndKospsPanel() {
|
||||
<button
|
||||
onClick={() => void fetchData()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -544,7 +544,7 @@ export default function RndKospsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 요약 통계 바 */}
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-xs text-t3 border-t border-stroke-1">
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-caption text-t3 border-t border-stroke-1">
|
||||
<span>
|
||||
정상 수신: <span className="text-emerald-400 font-medium">{totalReceived}건</span>
|
||||
</span>
|
||||
@ -567,7 +567,7 @@ export default function RndKospsPanel() {
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 파이프라인 현황 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<h3 className="text-xs font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
<h3 className="text-caption font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
데이터 파이프라인 현황
|
||||
</h3>
|
||||
<PipelineFlow nodes={pipeline} loading={loading} />
|
||||
@ -576,7 +576,7 @@ export default function RndKospsPanel() {
|
||||
{/* 필터 바 + 수신 이력 테이블 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between mb-3 gap-3 flex-wrap">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
데이터 수신 이력
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@ -584,7 +584,7 @@ export default function RndKospsPanel() {
|
||||
<select
|
||||
value={filterSource}
|
||||
onChange={(e) => setFilterSource(e.target.value as FilterSource)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (소스)</option>
|
||||
<option value="HYCOM">HYCOM</option>
|
||||
@ -595,7 +595,7 @@ export default function RndKospsPanel() {
|
||||
<select
|
||||
value={filterReceive}
|
||||
onChange={(e) => setFilterReceive(e.target.value as FilterReceive)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (상태)</option>
|
||||
<option value="수신완료">수신완료</option>
|
||||
@ -607,13 +607,13 @@ export default function RndKospsPanel() {
|
||||
<select
|
||||
value={filterPeriod}
|
||||
onChange={(e) => setFilterPeriod(e.target.value as FilterPeriod)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="6h">최근 6시간</option>
|
||||
<option value="12h">최근 12시간</option>
|
||||
<option value="24h">최근 24시간</option>
|
||||
</select>
|
||||
<span className="text-xs text-t3">{filteredLogs.length}건</span>
|
||||
<span className="text-caption text-t3">{filteredLogs.length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<DataLogTable rows={filteredLogs} loading={loading} />
|
||||
@ -621,7 +621,7 @@ export default function RndKospsPanel() {
|
||||
|
||||
{/* 알림 현황 */}
|
||||
<section className="px-5 pt-4 pb-5">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<AlertList alerts={alerts} loading={loading} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -324,7 +324,7 @@ function PipelineCard({ node }: { node: PipelineNode }) {
|
||||
<div
|
||||
className={`flex-1 min-w-0 bg-bg-card rounded border border-stroke-1 border-l-2 ${borderStyle} p-3 flex flex-col gap-1.5`}
|
||||
>
|
||||
<div className="text-xs font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<div className="text-caption font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<span
|
||||
className={`self-start inline-block px-1.5 py-0.5 rounded text-label-2 font-medium ${badgeStyle}`}
|
||||
>
|
||||
@ -343,7 +343,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<div className="flex-1 h-16 bg-bg-elevated rounded w-28" />
|
||||
{i < 4 && <span className="text-t3 text-sm px-0.5">→</span>}
|
||||
{i < 4 && <span className="text-t3 text-body-2 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -355,7 +355,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{nodes.map((node, idx) => (
|
||||
<div key={node.id} className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<PipelineCard node={node} />
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-sm shrink-0 px-0.5">→</span>}
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-body-2 shrink-0 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -394,7 +394,7 @@ const LOG_HEADERS = ['시간', '데이터소스', '데이터종류', '크기', '
|
||||
function DataLogTable({ rows, loading }: { rows: DataLogRow[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{LOG_HEADERS.map((h) => (
|
||||
@ -467,7 +467,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
}
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return <p className="text-xs text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
return <p className="text-caption text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -475,7 +475,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-xs ${getAlertStyle(alert.level)}`}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-caption ${getAlertStyle(alert.level)}`}
|
||||
>
|
||||
<span className="font-semibold shrink-0">[{alert.level}]</span>
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
@ -536,10 +536,10 @@ export default function RndPoseidonPanel() {
|
||||
{/* ── 헤더 ── */}
|
||||
<div className="shrink-0 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<h2 className="text-sm font-semibold text-t1">유출유확산예측 (포세이돈) 연계 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">유출유확산예측 (포세이돈) 연계 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -551,7 +551,7 @@ export default function RndPoseidonPanel() {
|
||||
<button
|
||||
onClick={() => void fetchData()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -571,7 +571,7 @@ export default function RndPoseidonPanel() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 요약 통계 바 */}
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-xs text-t3 border-t border-stroke-1">
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-caption text-t3 border-t border-stroke-1">
|
||||
<span>
|
||||
정상 수신: <span className="text-emerald-400 font-medium">{totalReceived}건</span>
|
||||
</span>
|
||||
@ -594,7 +594,7 @@ export default function RndPoseidonPanel() {
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 파이프라인 현황 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<h3 className="text-xs font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
<h3 className="text-caption font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
데이터 파이프라인 현황
|
||||
</h3>
|
||||
<PipelineFlow nodes={pipeline} loading={loading} />
|
||||
@ -603,7 +603,7 @@ export default function RndPoseidonPanel() {
|
||||
{/* 필터 바 + 수신 이력 테이블 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between mb-3 gap-3 flex-wrap">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
데이터 수신 이력
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@ -611,7 +611,7 @@ export default function RndPoseidonPanel() {
|
||||
<select
|
||||
value={filterSource}
|
||||
onChange={(e) => setFilterSource(e.target.value as FilterSource)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (소스)</option>
|
||||
<option value="HYCOM">HYCOM</option>
|
||||
@ -622,7 +622,7 @@ export default function RndPoseidonPanel() {
|
||||
<select
|
||||
value={filterReceive}
|
||||
onChange={(e) => setFilterReceive(e.target.value as FilterReceive)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (상태)</option>
|
||||
<option value="수신완료">수신완료</option>
|
||||
@ -634,13 +634,13 @@ export default function RndPoseidonPanel() {
|
||||
<select
|
||||
value={filterPeriod}
|
||||
onChange={(e) => setFilterPeriod(e.target.value as FilterPeriod)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="6h">최근 6시간</option>
|
||||
<option value="12h">최근 12시간</option>
|
||||
<option value="24h">최근 24시간</option>
|
||||
</select>
|
||||
<span className="text-xs text-t3">{filteredLogs.length}건</span>
|
||||
<span className="text-caption text-t3">{filteredLogs.length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<DataLogTable rows={filteredLogs} loading={loading} />
|
||||
@ -648,7 +648,7 @@ export default function RndPoseidonPanel() {
|
||||
|
||||
{/* 알림 현황 */}
|
||||
<section className="px-5 pt-4 pb-5">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<AlertList alerts={alerts} loading={loading} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -297,7 +297,7 @@ function PipelineCard({ node }: { node: PipelineNode }) {
|
||||
<div
|
||||
className={`flex-1 min-w-0 bg-bg-card rounded border border-stroke-1 border-l-2 ${borderStyle} p-3 flex flex-col gap-1.5`}
|
||||
>
|
||||
<div className="text-xs font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<div className="text-caption font-medium text-t1 leading-snug">{node.name}</div>
|
||||
<span
|
||||
className={`self-start inline-block px-1.5 py-0.5 rounded text-label-2 font-medium ${badgeStyle}`}
|
||||
>
|
||||
@ -316,7 +316,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-1">
|
||||
<div className="flex-1 h-16 bg-bg-elevated rounded w-28" />
|
||||
{i < 4 && <span className="text-t3 text-sm px-0.5">→</span>}
|
||||
{i < 4 && <span className="text-t3 text-body-2 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -328,7 +328,7 @@ function PipelineFlow({ nodes, loading }: { nodes: PipelineNode[]; loading: bool
|
||||
{nodes.map((node, idx) => (
|
||||
<div key={node.id} className="flex items-center gap-1 flex-1 min-w-0">
|
||||
<PipelineCard node={node} />
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-sm shrink-0 px-0.5">→</span>}
|
||||
{idx < nodes.length - 1 && <span className="text-t3 text-body-2 shrink-0 px-0.5">→</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -367,7 +367,7 @@ const LOG_HEADERS = ['시간', '데이터소스', '데이터종류', '크기', '
|
||||
function DataLogTable({ rows, loading }: { rows: DataLogRow[]; loading: boolean }) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{LOG_HEADERS.map((h) => (
|
||||
@ -440,7 +440,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
}
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return <p className="text-xs text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
return <p className="text-caption text-t3 py-2">활성 알림이 없습니다.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -448,7 +448,7 @@ function AlertList({ alerts, loading }: { alerts: AlertItem[]; loading: boolean
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-xs ${getAlertStyle(alert.level)}`}
|
||||
className={`flex items-start gap-2 px-3 py-2 rounded border text-caption ${getAlertStyle(alert.level)}`}
|
||||
>
|
||||
<span className="font-semibold shrink-0">[{alert.level}]</span>
|
||||
<span className="flex-1">{alert.message}</span>
|
||||
@ -509,10 +509,10 @@ export default function RndRescuePanel() {
|
||||
{/* ── 헤더 ── */}
|
||||
<div className="shrink-0 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<h2 className="text-sm font-semibold text-t1">긴급구난과제 연계 모니터링</h2>
|
||||
<h2 className="text-body-2 font-semibold text-t1">긴급구난과제 연계 모니터링</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{lastUpdate && (
|
||||
<span className="text-xs text-t3">
|
||||
<span className="text-caption text-t3">
|
||||
갱신:{' '}
|
||||
{lastUpdate.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
@ -524,7 +524,7 @@ export default function RndRescuePanel() {
|
||||
<button
|
||||
onClick={() => void fetchData()}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-caption rounded bg-bg-elevated hover:bg-bg-card text-t2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`}
|
||||
@ -544,7 +544,7 @@ export default function RndRescuePanel() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 요약 통계 바 */}
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-xs text-t3 border-t border-stroke-1">
|
||||
<div className="flex items-center gap-4 px-5 py-2 bg-bg-base text-caption text-t3 border-t border-stroke-1">
|
||||
<span>
|
||||
정상 수신: <span className="text-emerald-400 font-medium">{totalReceived}건</span>
|
||||
</span>
|
||||
@ -567,7 +567,7 @@ export default function RndRescuePanel() {
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* 파이프라인 현황 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<h3 className="text-xs font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
<h3 className="text-caption font-semibold text-t2 mb-3 uppercase tracking-wide">
|
||||
데이터 파이프라인 현황
|
||||
</h3>
|
||||
<PipelineFlow nodes={pipeline} loading={loading} />
|
||||
@ -576,7 +576,7 @@ export default function RndRescuePanel() {
|
||||
{/* 필터 바 + 수신 이력 테이블 */}
|
||||
<section className="px-5 pt-4 pb-3 border-b border-stroke-1">
|
||||
<div className="flex items-center justify-between mb-3 gap-3 flex-wrap">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide shrink-0">
|
||||
데이터 수신 이력
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@ -584,7 +584,7 @@ export default function RndRescuePanel() {
|
||||
<select
|
||||
value={filterSource}
|
||||
onChange={(e) => setFilterSource(e.target.value as FilterSource)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (소스)</option>
|
||||
<option value="HYCOM">HYCOM</option>
|
||||
@ -595,7 +595,7 @@ export default function RndRescuePanel() {
|
||||
<select
|
||||
value={filterReceive}
|
||||
onChange={(e) => setFilterReceive(e.target.value as FilterReceive)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="all">모두 (상태)</option>
|
||||
<option value="수신완료">수신완료</option>
|
||||
@ -607,13 +607,13 @@ export default function RndRescuePanel() {
|
||||
<select
|
||||
value={filterPeriod}
|
||||
onChange={(e) => setFilterPeriod(e.target.value as FilterPeriod)}
|
||||
className="px-2 py-1 text-xs rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
className="px-2 py-1 text-caption rounded bg-bg-elevated border border-stroke-1 text-t2 focus:outline-none focus:border-cyan-500 transition-colors"
|
||||
>
|
||||
<option value="6h">최근 6시간</option>
|
||||
<option value="12h">최근 12시간</option>
|
||||
<option value="24h">최근 24시간</option>
|
||||
</select>
|
||||
<span className="text-xs text-t3">{filteredLogs.length}건</span>
|
||||
<span className="text-caption text-t3">{filteredLogs.length}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<DataLogTable rows={filteredLogs} loading={loading} />
|
||||
@ -621,7 +621,7 @@ export default function RndRescuePanel() {
|
||||
|
||||
{/* 알림 현황 */}
|
||||
<section className="px-5 pt-4 pb-5">
|
||||
<h3 className="text-xs font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<h3 className="text-caption font-semibold text-t2 uppercase tracking-wide mb-3">알림 현황</h3>
|
||||
<AlertList alerts={alerts} loading={loading} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -193,12 +193,12 @@ function FrameworkTab() {
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
{/* 1. 개발 프레임워크 구성 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 개발 프레임워크 구성</h3>
|
||||
<div className="border border-stroke-1 rounded overflow-hidden">
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">1. 개발 프레임워크 구성</h3>
|
||||
<div className="border border-stroke rounded overflow-hidden">
|
||||
{/* 프레젠테이션 계층 */}
|
||||
<div className="border-b border-stroke-1 p-4 bg-bg-card">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">프레젠테이션 계층</p>
|
||||
<p className="text-xs text-t3 mb-3">React 19 + TypeScript 5.9 + Tailwind CSS 3</p>
|
||||
<div className="border-b border-stroke p-4 bg-bg-card">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">프레젠테이션 계층</p>
|
||||
<p className="text-caption text-t3 mb-3">React 19 + TypeScript 5.9 + Tailwind CSS 3</p>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ name: 'MapLibre', sub: 'GL JS 5' },
|
||||
@ -208,18 +208,18 @@ function FrameworkTab() {
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded p-2 text-center"
|
||||
>
|
||||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||||
<p className="text-caption font-medium text-t1">{item.name}</p>
|
||||
<p className="text-caption text-t3 mt-0.5">{item.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* 비즈니스 로직 계층 */}
|
||||
<div className="border-b border-stroke-1 p-4 bg-bg-surface">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">비즈니스 로직 계층</p>
|
||||
<p className="text-xs text-t3 mb-3">Express 4 + TypeScript</p>
|
||||
<div className="border-b border-stroke p-4 bg-bg-surface">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">비즈니스 로직 계층</p>
|
||||
<p className="text-caption text-t3 mb-3">Express 4 + TypeScript</p>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ name: 'JWT 인증', sub: 'OAuth2.0' },
|
||||
@ -229,18 +229,18 @@ function FrameworkTab() {
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded p-2 text-center"
|
||||
>
|
||||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||||
<p className="text-caption font-medium text-t1">{item.name}</p>
|
||||
<p className="text-caption text-t3 mt-0.5">{item.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* 데이터 접근 계층 */}
|
||||
<div className="p-4 bg-bg-card">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">데이터 접근 계층</p>
|
||||
<p className="text-xs text-t3 mb-3">PostgreSQL 16 + PostGIS</p>
|
||||
<p className="text-caption font-semibold text-t2 mb-1">데이터 접근 계층</p>
|
||||
<p className="text-caption text-t3 mb-3">PostgreSQL 16 + PostGIS</p>
|
||||
<div className="grid grid-cols-3 gap-2 max-w-xs">
|
||||
{[
|
||||
{ name: 'wing DB', sub: '운영 DB' },
|
||||
@ -249,10 +249,10 @@ function FrameworkTab() {
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded p-2 text-center"
|
||||
>
|
||||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||||
<p className="text-xs text-t3 mt-0.5">{item.sub}</p>
|
||||
<p className="text-caption font-medium text-t1">{item.name}</p>
|
||||
<p className="text-caption text-t3 mt-0.5">{item.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -262,15 +262,15 @@ function FrameworkTab() {
|
||||
|
||||
{/* 2. 기술 스택 상세 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 기술 스택 상세</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">2. 기술 스택 상세</h3>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['구분', '기술', '버전', '설명'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -279,7 +279,7 @@ function FrameworkTab() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{TECH_STACK.map((row, idx) => (
|
||||
<tr key={idx} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={idx} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">
|
||||
{row.category}
|
||||
</td>
|
||||
@ -295,7 +295,7 @@ function FrameworkTab() {
|
||||
|
||||
{/* 3. 개발 표준 및 규칙 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 개발 표준 및 규칙</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">3. 개발 표준 및 규칙</h3>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{[
|
||||
{
|
||||
@ -315,9 +315,9 @@ function FrameworkTab() {
|
||||
content: '입력 살균(sanitize), XSS/SQL Injection 방지, CORS 정책, Rate Limiting',
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||||
<div key={item.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{item.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -333,26 +333,26 @@ function TargetArchTab() {
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
{/* 1. 시스템 전체 구성도 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 시스템 전체 구성도</h3>
|
||||
<div className="flex flex-col items-stretch gap-0 border border-stroke-1 rounded overflow-hidden">
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">1. 시스템 전체 구성도</h3>
|
||||
<div className="flex flex-col items-stretch gap-0 border border-stroke rounded overflow-hidden">
|
||||
{/* 사용자 접근 계층 */}
|
||||
<div className="p-4 bg-bg-card">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">사용자 접근 계층</p>
|
||||
<p className="text-xs text-t1 font-medium mb-1">웹 브라우저 (React SPA)</p>
|
||||
<p className="text-xs text-t3 leading-relaxed">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">사용자 접근 계층</p>
|
||||
<p className="text-caption text-t1 font-medium mb-1">웹 브라우저 (React SPA)</p>
|
||||
<p className="text-caption text-t3 leading-relaxed">
|
||||
확산예측 | HNS분석 | 구조시나리오 | 항공방제 | 기상정보 | 사고관리 | SCAT조사 |
|
||||
자산관리 | 관리자
|
||||
</p>
|
||||
</div>
|
||||
{/* 화살표 + 프로토콜 */}
|
||||
<div className="flex flex-col items-center py-2 bg-bg-base border-y border-stroke-1">
|
||||
<div className="flex flex-col items-center py-2 bg-bg-base border-y border-stroke">
|
||||
<span className="text-t3 text-lg">↕</span>
|
||||
<span className="text-xs text-t3">HTTPS (TLS 1.2+)</span>
|
||||
<span className="text-caption text-t3">HTTPS (TLS 1.2+)</span>
|
||||
</div>
|
||||
{/* API 서버 계층 */}
|
||||
<div className="p-4 bg-bg-surface border-b border-stroke-1">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">API 서버 계층</p>
|
||||
<p className="text-xs text-t1 font-medium mb-2">Express 4 REST API (Port 3001)</p>
|
||||
<div className="p-4 bg-bg-surface border-b border-stroke">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">API 서버 계층</p>
|
||||
<p className="text-caption text-t1 font-medium mb-2">Express 4 REST API (Port 3001)</p>
|
||||
<div className="grid grid-cols-2 gap-1.5 sm:grid-cols-4">
|
||||
{[
|
||||
'JWT 인증 미들웨어',
|
||||
@ -362,22 +362,22 @@ function TargetArchTab() {
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1.5 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded px-2 py-1.5 text-center"
|
||||
>
|
||||
<p className="text-xs text-t2 leading-snug">{item}</p>
|
||||
<p className="text-caption text-t2 leading-snug">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* 화살표 + 프로토콜 */}
|
||||
<div className="flex flex-col items-center py-2 bg-bg-base border-b border-stroke-1">
|
||||
<div className="flex flex-col items-center py-2 bg-bg-base border-b border-stroke">
|
||||
<span className="text-t3 text-lg">↕</span>
|
||||
<span className="text-xs text-t3">pg connection pool</span>
|
||||
<span className="text-caption text-t3">pg connection pool</span>
|
||||
</div>
|
||||
{/* 데이터 계층 */}
|
||||
<div className="p-4 bg-bg-card">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">데이터 계층</p>
|
||||
<p className="text-xs text-t1 font-medium mb-2">PostgreSQL 16 + PostGIS</p>
|
||||
<p className="text-caption font-semibold text-t2 mb-1">데이터 계층</p>
|
||||
<p className="text-caption text-t1 font-medium mb-2">PostgreSQL 16 + PostGIS</p>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ name: 'wing DB', sub: '운영' },
|
||||
@ -385,10 +385,10 @@ function TargetArchTab() {
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded p-2 text-center min-w-24"
|
||||
className="bg-bg-elevated border border-stroke rounded p-2 text-center min-w-24"
|
||||
>
|
||||
<p className="text-xs font-medium text-t1">{item.name}</p>
|
||||
<p className="text-xs text-t3 mt-0.5">({item.sub})</p>
|
||||
<p className="text-caption font-medium text-t1">{item.name}</p>
|
||||
<p className="text-caption text-t3 mt-0.5">({item.sub})</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -398,15 +398,15 @@ function TargetArchTab() {
|
||||
|
||||
{/* 2. 탭 기반 업무 모듈 구조 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 탭 기반 업무 모듈 구조</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">2. 탭 기반 업무 모듈 구조</h3>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['모듈', '패키지명', '기능', '주요 연계'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -415,7 +415,7 @@ function TargetArchTab() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{TAB_MODULES.map((row) => (
|
||||
<tr key={row.name} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.name} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.module}</td>
|
||||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.name}</td>
|
||||
<td className="px-3 py-2 text-t2">{row.feature}</td>
|
||||
@ -429,7 +429,7 @@ function TargetArchTab() {
|
||||
|
||||
{/* 3. RBAC 권한 체계 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">3. RBAC 권한 체계</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">3. RBAC 권한 체계</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{[
|
||||
{
|
||||
@ -448,9 +448,9 @@ function TargetArchTab() {
|
||||
'누가(사용자) / 언제(타임스탬프) / 무엇을(기능) / 어디서(IP, 메뉴) — 모든 주요 작업 자동 기록',
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||||
<div key={item.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{item.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -468,17 +468,17 @@ function InterfaceTab() {
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
{/* 1. 외부 시스템 연계 구성도 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 외부 시스템 연계 구성도</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">1. 외부 시스템 연계 구성도</h3>
|
||||
<div className="flex items-stretch gap-2">
|
||||
{/* 외부 시스템 */}
|
||||
<div className="flex-1 bg-bg-card border border-stroke-1 rounded p-3 flex flex-col gap-1.5">
|
||||
<p className="text-xs font-semibold text-t2 mb-1 text-center">외부 시스템</p>
|
||||
<div className="flex-1 bg-bg-card border border-stroke rounded p-3 flex flex-col gap-1.5">
|
||||
<p className="text-caption font-semibold text-t2 mb-1 text-center">외부 시스템</p>
|
||||
{['KHOA API', '기상청 API', '해경 KBP', 'AIS 선박'].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded px-2 py-1 text-center"
|
||||
>
|
||||
<p className="text-xs text-t2">{item}</p>
|
||||
<p className="text-caption text-t2">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -488,17 +488,17 @@ function InterfaceTab() {
|
||||
<span className="text-t3 text-lg">←</span>
|
||||
</div>
|
||||
{/* 통합지원시스템 */}
|
||||
<div className="flex-[2] bg-bg-surface border-2 border-cyan-600/40 rounded p-3 flex flex-col gap-2">
|
||||
<p className="text-xs font-semibold text-t1 text-center">
|
||||
<div className="flex-[2] bg-bg-surface border-2 border-stroke rounded p-3 flex flex-col gap-2">
|
||||
<p className="text-caption font-semibold text-t1 text-center">
|
||||
해양환경 위기대응
|
||||
<br />
|
||||
통합지원시스템
|
||||
</p>
|
||||
<div className="border border-stroke-1 rounded p-2 bg-bg-elevated">
|
||||
<p className="text-xs font-medium text-t2 mb-1 text-center">연계관리 모듈</p>
|
||||
<div className="border border-stroke rounded p-2 bg-bg-elevated">
|
||||
<p className="text-caption font-medium text-t2 mb-1 text-center">연계관리 모듈</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
{['수집자료 관리', '연계 모니터링', '비식별화 조치'].map((item) => (
|
||||
<p key={item} className="text-xs text-t3 text-center">
|
||||
<p key={item} className="text-caption text-t3 text-center">
|
||||
- {item}
|
||||
</p>
|
||||
))}
|
||||
@ -511,14 +511,14 @@ function InterfaceTab() {
|
||||
<span className="text-t3 text-lg">→</span>
|
||||
</div>
|
||||
{/* R&D 시스템 */}
|
||||
<div className="flex-1 bg-bg-card border border-stroke-1 rounded p-3 flex flex-col gap-1.5">
|
||||
<p className="text-xs font-semibold text-t2 mb-1 text-center">R&D 시스템</p>
|
||||
<div className="flex-1 bg-bg-card border border-stroke rounded p-3 flex flex-col gap-1.5">
|
||||
<p className="text-caption font-semibold text-t2 mb-1 text-center">R&D 시스템</p>
|
||||
{['포세이돈', 'KOSPS', '충북대 HNS', '긴급구난'].map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="bg-bg-elevated border border-stroke-1 rounded px-2 py-1 text-center"
|
||||
className="bg-bg-elevated border border-stroke rounded px-2 py-1 text-center"
|
||||
>
|
||||
<p className="text-xs text-t2">{item}</p>
|
||||
<p className="text-caption text-t2">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -527,15 +527,15 @@ function InterfaceTab() {
|
||||
|
||||
{/* 2. 연계 인터페이스 목록 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 연계 인터페이스 목록</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">2. 연계 인터페이스 목록</h3>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['연계 시스템', '연계 방식', '데이터', '주기', '프로토콜'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -544,7 +544,7 @@ function InterfaceTab() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{INTERFACES.map((row) => (
|
||||
<tr key={row.system} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.system} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.system}</td>
|
||||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.method}</td>
|
||||
<td className="px-3 py-2 text-t2">{row.data}</td>
|
||||
@ -559,12 +559,12 @@ function InterfaceTab() {
|
||||
|
||||
{/* 3. 데이터 흐름도 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 데이터 흐름도</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">3. 데이터 흐름도</h3>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{dataFlowSteps.map((step, idx) => (
|
||||
<div key={step} className="flex items-center gap-1">
|
||||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-16">
|
||||
<p className="text-xs font-medium text-t1">{step}</p>
|
||||
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2 text-center min-w-16">
|
||||
<p className="text-caption font-medium text-t1">{step}</p>
|
||||
</div>
|
||||
{idx < dataFlowSteps.length - 1 && (
|
||||
<span className="text-t3 text-lg shrink-0">→</span>
|
||||
@ -581,9 +581,9 @@ function InterfaceTab() {
|
||||
{ step: '시각화', desc: 'MapLibre GL + deck.gl 기반 지도 레이어 렌더링' },
|
||||
{ step: '의사결정지원', desc: '방제작전 시나리오, 구조분석, 경보 발령 지원' },
|
||||
].map((item) => (
|
||||
<div key={item.step} className="bg-bg-card border border-stroke-1 rounded p-2.5">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">{item.step}</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{item.desc}</p>
|
||||
<div key={item.step} className="bg-bg-card border border-stroke rounded p-2.5">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">{item.step}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -591,7 +591,7 @@ function InterfaceTab() {
|
||||
|
||||
{/* 4. 연계 장애 대응 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 연계 장애 대응</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">4. 연계 장애 대응</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{[
|
||||
{
|
||||
@ -611,9 +611,9 @@ function InterfaceTab() {
|
||||
content: '개인정보 포함 데이터(해경 KBP 인사 등) 수집 시 자동 비식별화 처리 적용',
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{item.content}</p>
|
||||
<div key={item.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">{item.title}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{item.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -837,17 +837,17 @@ function HeterogeneousTab() {
|
||||
<div className="p-5 space-y-6">
|
||||
{/* 1. 이기종시스템 연계 개요 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 이기종시스템 연계 개요</h3>
|
||||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">1. 이기종시스템 연계 개요</h3>
|
||||
<p className="text-caption text-t2 leading-relaxed mb-4">
|
||||
통합지원시스템은 Fortran, Python, C++, Java 등 다양한 언어와 플랫폼으로 구현된 이기종
|
||||
시스템과 연계한다. REST API 표준화, ETL 전처리, 어댑터 패턴을 통해 언어·플랫폼 독립적인
|
||||
연계 구조를 구현하며, 외부망·내부망 이중 네트워크 정책을 준수한다.
|
||||
</p>
|
||||
<div className="flex items-stretch gap-2">
|
||||
<div className="flex-1 bg-bg-elevated border border-stroke-1 rounded p-3 text-center">
|
||||
<p className="text-xs font-semibold text-t2 mb-2">이기종 시스템</p>
|
||||
<div className="flex-1 bg-bg-elevated border border-stroke rounded p-3 text-center">
|
||||
<p className="text-caption font-semibold text-t2 mb-2">이기종 시스템</p>
|
||||
{['Fortran KOSPS', 'Python/C++ 충북대', 'Java 해경KBP', 'NetCDF HYCOM'].map((item) => (
|
||||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||||
<p key={item} className="text-caption text-t3 leading-relaxed">
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
@ -856,10 +856,10 @@ function HeterogeneousTab() {
|
||||
<span className="text-t3 text-lg">→</span>
|
||||
<span className="text-t3 text-lg">←</span>
|
||||
</div>
|
||||
<div className="flex-1 bg-cyan-600/10 border border-cyan-500/30 rounded p-3 text-center">
|
||||
<p className="text-xs font-semibold text-t2 mb-2">연계 어댑터 계층</p>
|
||||
<div className="flex-1 bg-[rgba(6,182,212,0.1)] border border-stroke rounded p-3 text-center">
|
||||
<p className="text-caption font-semibold text-t2 mb-2">연계 어댑터 계층</p>
|
||||
{['REST API 래퍼', 'ETL 전처리', '프로토콜 변환', '인증 통합'].map((item) => (
|
||||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||||
<p key={item} className="text-caption text-t3 leading-relaxed">
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
@ -868,10 +868,10 @@ function HeterogeneousTab() {
|
||||
<span className="text-t3 text-lg">→</span>
|
||||
<span className="text-t3 text-lg">←</span>
|
||||
</div>
|
||||
<div className="flex-1 bg-bg-elevated border border-stroke-1 rounded p-3 text-center">
|
||||
<p className="text-xs font-semibold text-t2 mb-2">통합지원시스템</p>
|
||||
<div className="flex-1 bg-bg-elevated border border-stroke rounded p-3 text-center">
|
||||
<p className="text-caption font-semibold text-t2 mb-2">통합지원시스템</p>
|
||||
{['Express REST API', 'PostgreSQL+PostGIS', 'React SPA', '표준 JSON'].map((item) => (
|
||||
<p key={item} className="text-xs text-t3 leading-relaxed">
|
||||
<p key={item} className="text-caption text-t3 leading-relaxed">
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
@ -881,18 +881,18 @@ function HeterogeneousTab() {
|
||||
|
||||
{/* 2. 이기종 시스템 간의 연계 방안 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 이기종 시스템 간의 연계 방안</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">2. 이기종 시스템 간의 연계 방안</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{INTEGRATION_PLANS.map((item, idx) => (
|
||||
<div key={item.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<p className="text-xs font-semibold text-t2 mb-1">
|
||||
<div key={item.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<p className="text-caption font-semibold text-t2 mb-1">
|
||||
{idx + 1}. {item.title}
|
||||
</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{item.description}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{item.description}</p>
|
||||
{item.details && (
|
||||
<ul className="mt-1.5 flex flex-col gap-1 pl-3">
|
||||
{item.details.map((detail) => (
|
||||
<li key={detail} className="text-xs text-t3 leading-relaxed list-disc">
|
||||
<li key={detail} className="text-caption text-t3 leading-relaxed list-disc">
|
||||
{detail}
|
||||
</li>
|
||||
))}
|
||||
@ -905,15 +905,15 @@ function HeterogeneousTab() {
|
||||
|
||||
{/* 3. 연계 대상 이기종 시스템 목록 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 연계 대상 이기종 시스템 목록</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">3. 연계 대상 이기종 시스템 목록</h3>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['시스템', '구현 언어', 'OS', '위치', '연계 프로토콜', '연계 설명'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -922,7 +922,7 @@ function HeterogeneousTab() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{HETEROGENEOUS_SYSTEMS.map((row) => (
|
||||
<tr key={row.system} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.system} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap">{row.system}</td>
|
||||
<td className="px-3 py-2 text-t2 whitespace-nowrap font-mono">{row.lang}</td>
|
||||
<td className="px-3 py-2 text-t2 whitespace-nowrap">{row.os}</td>
|
||||
@ -938,16 +938,16 @@ function HeterogeneousTab() {
|
||||
|
||||
{/* 4. 이기종 연계 전략 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 이기종 연계 전략</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">4. 이기종 연계 전략</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{HETEROGENEOUS_STRATEGIES.map((card) => (
|
||||
<div key={card.challenge} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<div key={card.challenge} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<span className="text-xs font-semibold text-red-400">{card.challenge}</span>
|
||||
<span className="text-t3 text-xs">→</span>
|
||||
<span className="text-xs font-semibold text-cyan-400">{card.solution}</span>
|
||||
<span className="text-caption font-semibold text-color-danger">{card.challenge}</span>
|
||||
<span className="text-t3 text-caption">→</span>
|
||||
<span className="text-caption font-semibold text-color-accent">{card.solution}</span>
|
||||
</div>
|
||||
<p className="text-xs text-t2 leading-relaxed">{card.description}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{card.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -955,12 +955,12 @@ function HeterogeneousTab() {
|
||||
|
||||
{/* 5. 이기종 데이터 변환 흐름 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">5. 이기종 데이터 변환 흐름</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">5. 이기종 데이터 변환 흐름</h3>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{HETEROGENEOUS_FLOW_STEPS.map((step, idx) => (
|
||||
<div key={step} className="flex items-center gap-1">
|
||||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-16">
|
||||
<p className="text-xs font-medium text-t1">{step}</p>
|
||||
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2 text-center min-w-16">
|
||||
<p className="text-caption font-medium text-t1">{step}</p>
|
||||
</div>
|
||||
{idx < HETEROGENEOUS_FLOW_STEPS.length - 1 && (
|
||||
<span className="text-t3 text-lg shrink-0">→</span>
|
||||
@ -972,14 +972,14 @@ function HeterogeneousTab() {
|
||||
|
||||
{/* 6. 이기종 연계 보안 정책 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">6. 이기종 연계 보안 정책</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">6. 이기종 연계 보안 정책</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{HETEROGENEOUS_SECURITY.map((card) => (
|
||||
<div key={card.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<p className="text-xs font-semibold text-t2 mb-2">{card.title}</p>
|
||||
<div key={card.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<p className="text-caption font-semibold text-t2 mb-2">{card.title}</p>
|
||||
<ul className="flex flex-col gap-1">
|
||||
{card.items.map((item) => (
|
||||
<li key={item} className="text-xs text-t2 leading-relaxed">
|
||||
<li key={item} className="text-caption text-t2 leading-relaxed">
|
||||
· {item}
|
||||
</li>
|
||||
))}
|
||||
@ -1065,7 +1065,7 @@ const COMMON_FEATURES: CommonFeatureItem[] = [
|
||||
description: 'Tailwind CSS @layer 아키텍처 + CSS 변수 디자인 시스템',
|
||||
details: [
|
||||
'@layer base → components → wing 3단계 CSS 계층 구조',
|
||||
'CSS 변수 기반 시맨틱 컬러 (bg-bg-base, text-t1, border-stroke-1 등)',
|
||||
'CSS 변수 기반 시맨틱 컬러 (bg-bg-base, text-t1, border-stroke 등)',
|
||||
'다크 모드 기본 적용 — CSS 변수 전환으로 테마 일괄 변경',
|
||||
'인라인 스타일 지양, Tailwind 유틸리티 클래스 우선',
|
||||
],
|
||||
@ -1509,8 +1509,8 @@ const FEATURE_MATRIX: FeatureMatrixRow[] = [
|
||||
];
|
||||
|
||||
const CATEGORY_STYLES: Record<string, string> = {
|
||||
공통기능: 'bg-cyan-600/20 text-cyan-300',
|
||||
기본정보관리: 'bg-emerald-600/20 text-emerald-300',
|
||||
공통기능: 'bg-[rgba(6,182,212,0.2)] text-color-accent',
|
||||
기본정보관리: 'bg-[rgba(34,197,94,0.2)] text-color-success',
|
||||
업무기능: 'bg-bg-elevated text-t3',
|
||||
};
|
||||
|
||||
@ -1521,8 +1521,8 @@ function CommonFeaturesTab() {
|
||||
<div className="flex flex-col gap-6 p-5">
|
||||
{/* 1. 방제대응 프로세스 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">1. 방제대응 프로세스</h3>
|
||||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">1. 방제대응 프로세스</h3>
|
||||
<p className="text-caption text-t2 leading-relaxed mb-4">
|
||||
해양오염 사고 발생 시 사고 접수부터 상황 종료까지의 단계별 대응 프로세스이며, 각 단계에서
|
||||
활용하는 시스템 모듈을 표시한다.
|
||||
</p>
|
||||
@ -1530,11 +1530,11 @@ function CommonFeaturesTab() {
|
||||
<div className="flex items-start gap-1 flex-wrap mb-4">
|
||||
{RESPONSE_PROCESS.map((step, idx) => (
|
||||
<div key={step.phase} className="flex items-start gap-1">
|
||||
<div className="bg-bg-elevated border border-stroke-1 rounded px-3 py-2 text-center min-w-20">
|
||||
<p className="text-xs font-semibold text-t1 mb-1">{step.phase}</p>
|
||||
<div className="bg-bg-elevated border border-stroke rounded px-3 py-2 text-center min-w-20">
|
||||
<p className="text-caption font-semibold text-t1 mb-1">{step.phase}</p>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{step.modules.map((mod) => (
|
||||
<span key={mod} className="text-[10px] text-cyan-400">
|
||||
<span key={mod} className="text-[10px] text-color-accent">
|
||||
{mod}
|
||||
</span>
|
||||
))}
|
||||
@ -1551,20 +1551,20 @@ function CommonFeaturesTab() {
|
||||
{RESPONSE_PROCESS.map((step, idx) => (
|
||||
<div
|
||||
key={step.phase}
|
||||
className="bg-bg-card border border-stroke-1 rounded p-3 flex items-start gap-3"
|
||||
className="bg-bg-card border border-stroke rounded p-3 flex items-start gap-3"
|
||||
>
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-cyan-600 text-white text-xs font-semibold shrink-0 mt-0.5">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-color-accent text-white text-caption font-semibold shrink-0 mt-0.5">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-semibold text-t1 mb-0.5">{step.phase}</p>
|
||||
<p className="text-xs text-t2 leading-relaxed">{step.description}</p>
|
||||
<p className="text-caption font-semibold text-t1 mb-0.5">{step.phase}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed">{step.description}</p>
|
||||
</div>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{step.modules.map((mod) => (
|
||||
<span
|
||||
key={mod}
|
||||
className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-cyan-600/20 text-cyan-300"
|
||||
className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-[rgba(6,182,212,0.2)] text-color-accent"
|
||||
>
|
||||
{mod}
|
||||
</span>
|
||||
@ -1577,30 +1577,30 @@ function CommonFeaturesTab() {
|
||||
|
||||
{/* 2. 시스템별 기능 유무 매트릭스 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">2. 시스템별 기능 유무 매트릭스</h3>
|
||||
<p className="text-xs text-t2 leading-relaxed mb-4">
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">2. 시스템별 기능 유무 매트릭스</h3>
|
||||
<p className="text-caption text-t2 leading-relaxed mb-4">
|
||||
각 시스템(업무 모듈)별 기능의 유무를 파악하여 공통기능, 기본정보 관리(사용자, 지도 등) 등
|
||||
통합할 수 있는 기능을 표시한다.{' '}
|
||||
<span className="text-cyan-400 font-medium">통합 대상</span> 기능은 공통 모듈로 일원화하여
|
||||
<span className="text-color-accent font-medium">통합 대상</span> 기능은 공통 모듈로 일원화하여
|
||||
중복 개발을 방지한다.
|
||||
</p>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 tracking-wide">
|
||||
<th className="px-2 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap sticky left-0 bg-bg-elevated z-10">
|
||||
<th className="px-2 py-2 text-left font-medium border-b border-stroke whitespace-nowrap sticky left-0 bg-bg-elevated z-10">
|
||||
기능
|
||||
</th>
|
||||
<th className="px-2 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap">
|
||||
<th className="px-2 py-2 text-center font-medium border-b border-stroke whitespace-nowrap">
|
||||
분류
|
||||
</th>
|
||||
<th className="px-2 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap">
|
||||
<th className="px-2 py-2 text-center font-medium border-b border-stroke whitespace-nowrap">
|
||||
통합
|
||||
</th>
|
||||
{SYSTEM_MODULES.map((mod) => (
|
||||
<th
|
||||
key={mod}
|
||||
className="px-1.5 py-2 text-center font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-1.5 py-2 text-center font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
<span className="writing-mode-vertical text-[10px]">{mod}</span>
|
||||
</th>
|
||||
@ -1609,7 +1609,7 @@ function CommonFeaturesTab() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{FEATURE_MATRIX.map((row) => (
|
||||
<tr key={row.feature} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.feature} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-2 py-1.5 font-medium text-t1 whitespace-nowrap sticky left-0 bg-bg-base z-10">
|
||||
{row.feature}
|
||||
</td>
|
||||
@ -1622,7 +1622,7 @@ function CommonFeaturesTab() {
|
||||
</td>
|
||||
<td className="px-2 py-1.5 text-center">
|
||||
{row.integrated ? (
|
||||
<span className="text-cyan-400 font-semibold">통합</span>
|
||||
<span className="text-color-accent font-semibold">통합</span>
|
||||
) : (
|
||||
<span className="text-t3">개별</span>
|
||||
)}
|
||||
@ -1630,7 +1630,7 @@ function CommonFeaturesTab() {
|
||||
{SYSTEM_MODULES.map((mod) => (
|
||||
<td key={mod} className="px-1.5 py-1.5 text-center">
|
||||
{row.systems[mod] ? (
|
||||
<span className="text-emerald-400 font-bold">O</span>
|
||||
<span className="text-color-success font-bold">O</span>
|
||||
) : (
|
||||
<span className="text-t3/30">-</span>
|
||||
)}
|
||||
@ -1644,42 +1644,42 @@ function CommonFeaturesTab() {
|
||||
{/* 범례 */}
|
||||
<div className="flex gap-4 mt-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-cyan-600/20 text-cyan-300">
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-[rgba(6,182,212,0.2)] text-color-accent">
|
||||
공통기능
|
||||
</span>
|
||||
<span className="text-xs text-t3">전 모듈 공통 적용</span>
|
||||
<span className="text-caption text-t3">전 모듈 공통 적용</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-emerald-600/20 text-emerald-300">
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-[rgba(34,197,94,0.2)] text-color-success">
|
||||
기본정보관리
|
||||
</span>
|
||||
<span className="text-xs text-t3">사용자·지도·메뉴·설정 통합 관리</span>
|
||||
<span className="text-caption text-t3">사용자·지도·메뉴·설정 통합 관리</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-bg-elevated text-t3">
|
||||
업무기능
|
||||
</span>
|
||||
<span className="text-xs text-t3">모듈별 고유 기능</span>
|
||||
<span className="text-caption text-t3">모듈별 고유 기능</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. 공통기능 상세 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">3. 공통기능 상세</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">3. 공통기능 상세</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{COMMON_FEATURES.map((feature, idx) => (
|
||||
<div key={feature.title} className="bg-bg-card border border-stroke-1 rounded p-3">
|
||||
<div key={feature.title} className="bg-bg-card border border-stroke rounded p-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-cyan-600 text-white text-xs font-semibold shrink-0">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded bg-color-accent text-white text-caption font-semibold shrink-0">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<p className="text-xs font-semibold text-t1">{feature.title}</p>
|
||||
<p className="text-caption font-semibold text-t1">{feature.title}</p>
|
||||
</div>
|
||||
<p className="text-xs text-t2 leading-relaxed mb-2 pl-7">{feature.description}</p>
|
||||
<p className="text-caption text-t2 leading-relaxed mb-2 pl-7">{feature.description}</p>
|
||||
<ul className="flex flex-col gap-1 pl-7">
|
||||
{feature.details.map((detail) => (
|
||||
<li key={detail} className="text-xs text-t3 leading-relaxed list-disc">
|
||||
<li key={detail} className="text-caption text-t3 leading-relaxed list-disc">
|
||||
{detail}
|
||||
</li>
|
||||
))}
|
||||
@ -1691,15 +1691,15 @@ function CommonFeaturesTab() {
|
||||
|
||||
{/* 4. 공통 모듈 구조 */}
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-t1 mb-3">4. 공통 모듈 디렉토리 구조</h3>
|
||||
<h3 className="text-body-2 font-semibold text-t1 mb-3">4. 공통 모듈 디렉토리 구조</h3>
|
||||
<div className="overflow-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<table className="w-full text-caption border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-elevated text-t3 uppercase tracking-wide">
|
||||
{['디렉토리', '역할', '주요 파일'].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke-1 whitespace-nowrap"
|
||||
className="px-3 py-2 text-left font-medium border-b border-stroke whitespace-nowrap"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
@ -1746,7 +1746,7 @@ function CommonFeaturesTab() {
|
||||
{ dir: 'common/constants/', role: '상수 정의', files: 'featureIds.ts' },
|
||||
{ dir: 'common/data/', role: 'UI 데이터', files: 'layerData.ts (레이어 트리)' },
|
||||
].map((row) => (
|
||||
<tr key={row.dir} className="border-b border-stroke-1 hover:bg-bg-surface/50">
|
||||
<tr key={row.dir} className="border-b border-stroke hover:bg-bg-surface/50">
|
||||
<td className="px-3 py-2 font-medium text-t1 whitespace-nowrap font-mono">
|
||||
{row.dir}
|
||||
</td>
|
||||
@ -1770,19 +1770,19 @@ export default function SystemArchPanel() {
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke-1 shrink-0">
|
||||
<h2 className="text-sm font-semibold text-t1">시스템구조</h2>
|
||||
<div className="flex items-center justify-between px-5 py-3 border-b border-stroke shrink-0">
|
||||
<h2 className="text-body-2 font-semibold text-t1">시스템구조</h2>
|
||||
</div>
|
||||
|
||||
{/* 탭 버튼 */}
|
||||
<div className="flex gap-1.5 px-5 py-2.5 border-b border-stroke-1 shrink-0 bg-bg-base">
|
||||
<div className="flex gap-1.5 px-5 py-2.5 border-b border-stroke shrink-0 bg-bg-base">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
|
||||
className={`px-3 py-1.5 text-caption font-medium rounded transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-cyan-600 text-white'
|
||||
? 'bg-color-accent text-white'
|
||||
: 'bg-bg-elevated text-t2 hover:bg-bg-card'
|
||||
}`}
|
||||
>
|
||||
|
||||
@ -108,7 +108,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
{/* 계정 */}
|
||||
<div>
|
||||
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
|
||||
계정 <span className="text-red-400">*</span>
|
||||
계정 <span className="text-color-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -122,7 +122,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
{/* 비밀번호 */}
|
||||
<div>
|
||||
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
|
||||
비밀번호 <span className="text-red-400">*</span>
|
||||
비밀번호 <span className="text-color-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
@ -136,7 +136,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
{/* 사용자명 */}
|
||||
<div>
|
||||
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
|
||||
사용자명 <span className="text-red-400">*</span>
|
||||
사용자명 <span className="text-color-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -209,7 +209,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
return (
|
||||
<label
|
||||
key={role.sn}
|
||||
className="flex items-center gap-2 px-2 py-1.5 hover:bg-[rgba(255,255,255,0.04)] rounded cursor-pointer"
|
||||
className="flex items-center gap-2 px-2 py-1.5 hover:bg-[rgba(6,182,212,0.08)] rounded cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -229,7 +229,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
{error && <p className="text-label-2 text-red-400 font-korean">{error}</p>}
|
||||
{error && <p className="text-label-2 text-color-danger font-korean">{error}</p>}
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
@ -237,7 +237,7 @@ function RegisterModal({ allRoles, allOrgs, onClose, onSuccess }: RegisterModalP
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
@ -433,14 +433,14 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<button
|
||||
onClick={handleResetPassword}
|
||||
disabled={resetPwLoading || !newPassword.trim()}
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-yellow-400 text-yellow-400 hover:bg-[rgba(250,204,21,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-color-caution text-color-caution hover:bg-[rgba(234,179,8,0.12)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
>
|
||||
{resetPwLoading ? '초기화 중...' : resetPwDone ? '초기화 완료' : '비밀번호 초기화'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={unlockLoading || user.status !== 'LOCKED'}
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-color-success text-color-success hover:bg-[rgba(34,197,94,0.12)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
title={user.status !== 'LOCKED' ? '잠금 상태가 아닙니다' : ''}
|
||||
>
|
||||
{unlockLoading ? '해제 중...' : '패스워드잠금해제'}
|
||||
@ -469,7 +469,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
{(statusLabels[user.status] || statusLabels.INACTIVE).label}
|
||||
</span>
|
||||
{user.failCount > 0 && (
|
||||
<span className="text-caption text-red-400 font-korean">
|
||||
<span className="text-caption text-color-danger font-korean">
|
||||
(로그인 실패 {user.failCount}회)
|
||||
</span>
|
||||
)}
|
||||
@ -484,7 +484,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={unlockLoading}
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-green-400 text-green-400 hover:bg-[rgba(74,222,128,0.1)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
className="px-4 py-1.5 text-label-2 font-semibold rounded-md border border-color-success text-color-success hover:bg-[rgba(34,197,94,0.12)] transition-all disabled:opacity-50 font-korean flex-shrink-0"
|
||||
>
|
||||
{unlockLoading ? '해제 중...' : '잠금 해제'}
|
||||
</button>
|
||||
@ -522,8 +522,8 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<div
|
||||
className={`px-3 py-2 text-label-2 rounded-md font-korean ${
|
||||
message.type === 'success'
|
||||
? 'text-green-400 bg-[rgba(74,222,128,0.08)] border border-[rgba(74,222,128,0.2)]'
|
||||
: 'text-red-400 bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)]'
|
||||
? 'text-color-success bg-[rgba(34,197,94,0.08)] border border-[rgba(34,197,94,0.3)]'
|
||||
: 'text-color-danger bg-[rgba(239,68,68,0.08)] border border-[rgba(239,68,68,0.2)]'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
@ -535,7 +535,7 @@ function UserDetailModal({ user, allOrgs, onClose, onUpdated }: UserDetailModalP
|
||||
<div className="flex items-center justify-end px-6 py-3 border-t border-stroke">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-4 py-2 text-caption border border-stroke text-fg-sub rounded-md hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
@ -685,7 +685,7 @@ function UsersPanel() {
|
||||
</p>
|
||||
</div>
|
||||
{pendingCount > 0 && (
|
||||
<span className="px-2.5 py-1 text-caption font-bold rounded-full bg-[rgba(250,204,21,0.15)] text-yellow-400 border border-[rgba(250,204,21,0.3)] animate-pulse font-korean">
|
||||
<span className="px-2.5 py-1 text-caption font-bold rounded-full bg-[rgba(234,179,8,0.15)] text-color-caution border border-[rgba(234,179,8,0.3)] animate-pulse font-korean">
|
||||
승인대기 {pendingCount}명
|
||||
</span>
|
||||
)}
|
||||
@ -747,31 +747,31 @@ function UsersPanel() {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-stroke bg-bg-surface">
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean w-10">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean w-10">
|
||||
번호
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-mono">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-mono">
|
||||
ID
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
사용자명
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
직급
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
소속
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
이메일
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
역할
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-left text-caption font-semibold text-fg-disabled font-korean">
|
||||
승인상태
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-label-2 font-semibold text-fg-disabled font-korean">
|
||||
<th className="px-4 py-3 text-right text-caption font-semibold text-fg-disabled font-korean">
|
||||
관리
|
||||
</th>
|
||||
</tr>
|
||||
@ -793,15 +793,15 @@ function UsersPanel() {
|
||||
return (
|
||||
<tr
|
||||
key={user.id}
|
||||
className="border-b border-stroke hover:bg-[rgba(255,255,255,0.02)] transition-colors"
|
||||
className="border-b border-stroke hover:bg-[rgba(6,182,212,0.04)] transition-colors"
|
||||
>
|
||||
{/* 번호 */}
|
||||
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono text-center">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled font-mono text-center">
|
||||
{rowNum}
|
||||
</td>
|
||||
|
||||
{/* ID(account) */}
|
||||
<td className="px-4 py-3 text-label-1 text-fg-sub font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-sub font-mono">
|
||||
{user.account}
|
||||
</td>
|
||||
|
||||
@ -809,24 +809,24 @@ function UsersPanel() {
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setDetailUser(user)}
|
||||
className="text-label-1 text-color-accent font-semibold font-korean hover:underline"
|
||||
className="text-caption text-color-accent font-semibold font-korean hover:underline"
|
||||
>
|
||||
{user.name}
|
||||
</button>
|
||||
</td>
|
||||
|
||||
{/* 직급 */}
|
||||
<td className="px-4 py-3 text-label-1 text-fg-sub font-korean">
|
||||
<td className="px-4 py-3 text-caption text-fg-sub font-korean">
|
||||
{user.rank || '-'}
|
||||
</td>
|
||||
|
||||
{/* 소속 */}
|
||||
<td className="px-4 py-3 text-label-1 text-fg-sub font-korean">
|
||||
<td className="px-4 py-3 text-caption text-fg-sub font-korean">
|
||||
{user.orgAbbr || user.orgName || '-'}
|
||||
</td>
|
||||
|
||||
{/* 이메일 */}
|
||||
<td className="px-4 py-3 text-label-2 text-fg-disabled font-mono">
|
||||
<td className="px-4 py-3 text-caption text-fg-disabled font-mono">
|
||||
{user.email || '-'}
|
||||
</td>
|
||||
|
||||
@ -839,19 +839,13 @@ function UsersPanel() {
|
||||
title="클릭하여 역할 변경"
|
||||
>
|
||||
{user.roles.length > 0 ? (
|
||||
user.roles.map((roleCode, roleIdx) => {
|
||||
const color = getRoleColor(roleCode, roleIdx);
|
||||
user.roles.map((roleCode) => {
|
||||
const roleName =
|
||||
allRoles.find((r) => r.code === roleCode)?.name || roleCode;
|
||||
return (
|
||||
<span
|
||||
key={roleCode}
|
||||
className="px-2 py-0.5 text-caption font-semibold rounded-md font-korean"
|
||||
style={{
|
||||
background: `${color}20`,
|
||||
color: color,
|
||||
border: `1px solid ${color}40`,
|
||||
}}
|
||||
className="px-2 py-0.5 text-caption font-semibold rounded-md font-korean text-fg-sub bg-bg-elevated border border-stroke-light"
|
||||
>
|
||||
{roleName}
|
||||
</span>
|
||||
@ -889,7 +883,7 @@ function UsersPanel() {
|
||||
return (
|
||||
<label
|
||||
key={role.sn}
|
||||
className="flex items-center gap-2 px-2 py-1.5 hover:bg-[rgba(255,255,255,0.04)] rounded cursor-pointer"
|
||||
className="flex items-center gap-2 px-2 py-1.5 hover:bg-[rgba(6,182,212,0.08)] rounded cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -943,13 +937,13 @@ function UsersPanel() {
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleApprove(user.id)}
|
||||
className="px-2 py-1 text-caption font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
|
||||
className="px-2 py-1 text-caption font-semibold text-color-success border border-color-success rounded hover:bg-[rgba(34,197,94,0.12)] transition-all font-korean"
|
||||
>
|
||||
승인
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReject(user.id)}
|
||||
className="px-2 py-1 text-caption font-semibold text-red-400 border border-red-400 rounded hover:bg-[rgba(248,113,113,0.1)] transition-all font-korean"
|
||||
className="px-2 py-1 text-caption font-semibold text-color-danger border border-color-danger rounded hover:bg-[rgba(239,68,68,0.12)] transition-all font-korean"
|
||||
>
|
||||
거절
|
||||
</button>
|
||||
@ -958,7 +952,7 @@ function UsersPanel() {
|
||||
{user.status === 'LOCKED' && (
|
||||
<button
|
||||
onClick={() => handleUnlock(user.id)}
|
||||
className="px-2 py-1 text-caption font-semibold text-yellow-400 border border-yellow-400 rounded hover:bg-[rgba(250,204,21,0.1)] transition-all font-korean"
|
||||
className="px-2 py-1 text-caption font-semibold text-color-caution border border-color-caution rounded hover:bg-[rgba(234,179,8,0.12)] transition-all font-korean"
|
||||
>
|
||||
잠금해제
|
||||
</button>
|
||||
@ -966,7 +960,7 @@ function UsersPanel() {
|
||||
{user.status === 'ACTIVE' && (
|
||||
<button
|
||||
onClick={() => handleDeactivate(user.id)}
|
||||
className="px-2 py-1 text-caption font-semibold text-fg-disabled border border-stroke rounded hover:bg-[rgba(255,255,255,0.04)] transition-all font-korean"
|
||||
className="px-2 py-1 text-caption font-semibold text-fg-disabled border border-stroke rounded hover:bg-[rgba(6,182,212,0.08)] transition-all font-korean"
|
||||
>
|
||||
비활성화
|
||||
</button>
|
||||
@ -974,7 +968,7 @@ function UsersPanel() {
|
||||
{(user.status === 'INACTIVE' || user.status === 'REJECTED') && (
|
||||
<button
|
||||
onClick={() => handleActivate(user.id)}
|
||||
className="px-2 py-1 text-caption font-semibold text-green-400 border border-green-400 rounded hover:bg-[rgba(74,222,128,0.1)] transition-all font-korean"
|
||||
className="px-2 py-1 text-caption font-semibold text-color-success border border-color-success rounded hover:bg-[rgba(34,197,94,0.12)] transition-all font-korean"
|
||||
>
|
||||
활성화
|
||||
</button>
|
||||
@ -1001,7 +995,7 @@ function UsersPanel() {
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
|
||||
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(6,182,212,0.08)] disabled:opacity-40 transition-all font-korean"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
@ -1045,7 +1039,7 @@ function UsersPanel() {
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(255,255,255,0.04)] disabled:opacity-40 transition-all font-korean"
|
||||
className="px-2.5 py-1 text-label-2 border border-stroke text-fg-disabled rounded hover:bg-[rgba(6,182,212,0.08)] disabled:opacity-40 transition-all font-korean"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
|
||||
@ -10,18 +10,10 @@ interface SignalSlot {
|
||||
}
|
||||
|
||||
// ─── 상수 ──────────────────────────────────────────────────
|
||||
const SOURCE_COLORS: Record<SignalSource, string> = {
|
||||
VTS: '#3b82f6',
|
||||
'VTS-AIS': '#a855f7',
|
||||
'V-PASS': '#22c55e',
|
||||
'E-NAVI': '#f97316',
|
||||
'S&P AIS': '#ec4899',
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
ok: '#22c55e',
|
||||
warn: '#eab308',
|
||||
error: '#ef4444',
|
||||
ok: 'var(--color-success)',
|
||||
warn: 'var(--color-caution)',
|
||||
error: 'var(--color-danger)',
|
||||
none: 'rgba(255,255,255,0.06)',
|
||||
};
|
||||
|
||||
@ -172,7 +164,6 @@ export default function VesselSignalPanel() {
|
||||
{/* 시간축 높이 맞춤 빈칸 */}
|
||||
<div className="h-5 mb-3" />
|
||||
{SIGNAL_SOURCES.map((src) => {
|
||||
const c = SOURCE_COLORS[src];
|
||||
const st = stats.find((s) => s.src === src)!;
|
||||
return (
|
||||
<div
|
||||
@ -180,10 +171,10 @@ export default function VesselSignalPanel() {
|
||||
className="flex flex-col justify-center mb-4"
|
||||
style={{ height: 20 }}
|
||||
>
|
||||
<span className="text-label-1 font-semibold leading-tight" style={{ color: c }}>
|
||||
<span className="text-label-1 font-semibold leading-tight text-fg">
|
||||
{src}
|
||||
</span>
|
||||
<span className="text-caption font-mono text-text-4 mt-0.5">{st.rate}%</span>
|
||||
<span className="text-caption font-mono text-fg-sub mt-0.5">{st.rate}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -39,7 +39,7 @@ interface HNSLeftPanelProps {
|
||||
incidentCoord: { lon: number; lat: number } | null;
|
||||
onCoordChange: (coord: { lon: number; lat: number } | null) => void;
|
||||
onMapSelectClick: () => void;
|
||||
onRunPrediction: () => void;
|
||||
onRunPrediction: (params?: HNSInputParams) => void;
|
||||
isRunningPrediction: boolean;
|
||||
onParamsChange?: (params: HNSInputParams) => void;
|
||||
onReset?: () => void;
|
||||
@ -163,26 +163,30 @@ export function HNSLeftPanel({
|
||||
}
|
||||
};
|
||||
|
||||
// 현재 폼 state → HNSInputParams 스냅샷 (useEffect + 버튼 onClick 공용)
|
||||
const buildCurrentParams = (): HNSInputParams => ({
|
||||
substance,
|
||||
releaseType,
|
||||
emissionRate: parseFloat(emissionRate) || tox.Q,
|
||||
totalRelease: parseFloat(totalRelease) || tox.QTotal,
|
||||
releaseHeight: parseFloat(releaseHeight) || 0.5,
|
||||
releaseDuration: parseFloat(releaseDuration) || 300,
|
||||
poolRadius: parseFloat(poolRadius) || tox.poolRadius,
|
||||
algorithm,
|
||||
criteriaModel,
|
||||
weather,
|
||||
accidentDate,
|
||||
accidentTime,
|
||||
predictionTime,
|
||||
accidentName,
|
||||
});
|
||||
|
||||
// 파라미터 변경 시 부모에 통지
|
||||
useEffect(() => {
|
||||
if (onParamsChange) {
|
||||
onParamsChange({
|
||||
substance,
|
||||
releaseType,
|
||||
emissionRate: parseFloat(emissionRate) || tox.Q,
|
||||
totalRelease: parseFloat(totalRelease) || tox.QTotal,
|
||||
releaseHeight: parseFloat(releaseHeight) || 0.5,
|
||||
releaseDuration: parseFloat(releaseDuration) || 300,
|
||||
poolRadius: parseFloat(poolRadius) || tox.poolRadius,
|
||||
algorithm,
|
||||
criteriaModel,
|
||||
weather,
|
||||
accidentDate,
|
||||
accidentTime,
|
||||
predictionTime,
|
||||
accidentName,
|
||||
});
|
||||
onParamsChange(buildCurrentParams());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
substance,
|
||||
releaseType,
|
||||
@ -709,7 +713,7 @@ export function HNSLeftPanel({
|
||||
<button
|
||||
className="prd-btn pri"
|
||||
style={{ padding: '7px', fontSize: 'var(--font-size-label-2)' }}
|
||||
onClick={onRunPrediction}
|
||||
onClick={() => onRunPrediction(buildCurrentParams())}
|
||||
disabled={isRunningPrediction}
|
||||
>
|
||||
{isRunningPrediction ? '⏳ 실행 중...' : '대기확산 예측 실행'}
|
||||
|
||||
@ -189,12 +189,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRun}
|
||||
className="flex-[2] py-2.5 text-label-1 font-bold rounded-md text-white"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
background: 'linear-gradient(135deg, var(--color-accent), #ef4444)',
|
||||
border: 'none',
|
||||
}}
|
||||
className="flex-[2] py-2.5 text-label-1 font-bold rounded-md cursor-pointer transition-colors bg-bg-card border border-stroke text-fg-sub hover:bg-[rgba(6,182,212,0.15)] hover:border-[rgba(6,182,212,0.3)] hover:text-color-accent"
|
||||
>
|
||||
🔄 재계산 실행
|
||||
</button>
|
||||
|
||||
@ -197,6 +197,33 @@ function HNSManualViewer() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 히트맵 포인트에서 지도 자동 줌용 바운드 산정 (15% 패딩) */
|
||||
function computeBoundsFromHeatmap(
|
||||
points: { lon: number; lat: number; concentration: number }[],
|
||||
threshold = 0.01,
|
||||
): { north: number; south: number; east: number; west: number } | null {
|
||||
const filtered = points.filter((p) => p.concentration > threshold);
|
||||
if (filtered.length === 0) return null;
|
||||
let minLon = Infinity,
|
||||
maxLon = -Infinity,
|
||||
minLat = Infinity,
|
||||
maxLat = -Infinity;
|
||||
for (const p of filtered) {
|
||||
if (p.lon < minLon) minLon = p.lon;
|
||||
if (p.lon > maxLon) maxLon = p.lon;
|
||||
if (p.lat < minLat) minLat = p.lat;
|
||||
if (p.lat > maxLat) maxLat = p.lat;
|
||||
}
|
||||
const padLon = (maxLon - minLon) * 0.15 || 0.005;
|
||||
const padLat = (maxLat - minLat) * 0.15 || 0.005;
|
||||
return {
|
||||
west: minLon - padLon,
|
||||
east: maxLon + padLon,
|
||||
south: minLat - padLat,
|
||||
north: maxLat + padLat,
|
||||
};
|
||||
}
|
||||
|
||||
/* ─── 메인 HNSView ─── */
|
||||
export function HNSView() {
|
||||
const { activeSubTab, setActiveSubTab } = useSubMenu('hns');
|
||||
@ -205,6 +232,12 @@ export function HNSView() {
|
||||
const [rightCollapsed, setRightCollapsed] = useState(false);
|
||||
const [incidentCoord, setIncidentCoord] = useState<{ lon: number; lat: number } | null>(null);
|
||||
const [flyToCoord, setFlyToCoord] = useState<{ lon: number; lat: number } | undefined>(undefined);
|
||||
const [fitBoundsTarget, setFitBoundsTarget] = useState<{
|
||||
north: number;
|
||||
south: number;
|
||||
east: number;
|
||||
west: number;
|
||||
} | null>(null);
|
||||
const [isSelectingLocation, setIsSelectingLocation] = useState(false);
|
||||
const [mapBounds, setMapBounds] = useState<MapBounds | null>(null);
|
||||
const vessels = useVesselSignals(mapBounds);
|
||||
@ -307,11 +340,14 @@ export function HNSView() {
|
||||
poolRadius: params?.poolRadius ?? tox.poolRadius,
|
||||
};
|
||||
|
||||
// 동적 sim 도메인 — 풍속 기반 특성 거리 L 산정 (10km~50km)
|
||||
const advectMax = meteo.windSpeed * 3600; // 1시간 이류 거리
|
||||
const L = Math.max(10_000, Math.min(50_000, advectMax));
|
||||
const sim: SimParams = {
|
||||
xRange: [-100, 10000],
|
||||
yRange: [-2000, 2000],
|
||||
nx: 300,
|
||||
ny: 200,
|
||||
xRange: [-L * 0.05, L],
|
||||
yRange: [-L * 0.4, L * 0.4],
|
||||
nx: 400,
|
||||
ny: 320,
|
||||
zRef: 1.5,
|
||||
tStart: 0,
|
||||
tEnd: 600,
|
||||
@ -447,6 +483,7 @@ export function HNSView() {
|
||||
angle: meteo.windDirDeg,
|
||||
},
|
||||
].filter((z) => z.radius > 0),
|
||||
contours: resultForZones.contours,
|
||||
timestamp: new Date().toISOString(),
|
||||
windDirection: meteo.windDirDeg,
|
||||
substance: substanceName,
|
||||
@ -458,6 +495,10 @@ export function HNSView() {
|
||||
maxConcentration: resultForZones.maxConcentration,
|
||||
});
|
||||
|
||||
// 지도 자동 줌 — 확산 영역 전체를 화면에 맞춤
|
||||
const bounds = computeBoundsFromHeatmap(resultForZones.points);
|
||||
if (bounds) setFitBoundsTarget(bounds);
|
||||
|
||||
// 2. 분석 레코드 DB 저장 (비동기, 실패해도 무시)
|
||||
try {
|
||||
const acdntDtm =
|
||||
@ -654,11 +695,17 @@ export function HNSView() {
|
||||
alert('좌표 정보가 없어 분석을 실행할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
// spilUnitCd: 'g' = 순간 유출(totalRelease), 그 외('g/s') = 연속/dense_gas(emissionRate)
|
||||
const isInstant = analysis.spilUnitCd === 'g';
|
||||
const releaseType: HNSInputParams['releaseType'] = isInstant ? '순간 유출' : '연속 유출';
|
||||
const spilQty = analysis.spilQty ?? undefined;
|
||||
rslt = {
|
||||
coord,
|
||||
inputParams: {
|
||||
substance: analysis.sbstNm ?? '톨루엔 (Toluene)',
|
||||
totalRelease: analysis.spilQty ?? 5000,
|
||||
releaseType,
|
||||
emissionRate: !isInstant ? (spilQty ?? 10) : 10,
|
||||
totalRelease: isInstant ? (spilQty ?? 5000) : 5000,
|
||||
algorithm: analysis.algoCd ?? 'ALOHA (EPA)',
|
||||
criteriaModel: analysis.critMdlCd ?? 'AEGL',
|
||||
accidentDate: dateStr ?? '',
|
||||
@ -681,7 +728,7 @@ export function HNSView() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 좌표 복원
|
||||
// 좌표 복원 (fit-bounds는 runComputation 후 자동)
|
||||
setIncidentCoord(savedCoord);
|
||||
|
||||
// 입력 파라미터 복원 → HNSLeftPanel에 전달
|
||||
@ -744,23 +791,24 @@ export function HNSView() {
|
||||
zones: [
|
||||
{
|
||||
level: 'AEGL-3',
|
||||
color: 'rgba(59,130,246,0.4)',
|
||||
color: '#ef4444',
|
||||
radius: resultForZones.aeglDistances.aegl3,
|
||||
angle: meteo.windDirDeg,
|
||||
},
|
||||
{
|
||||
level: 'AEGL-2',
|
||||
color: 'rgba(6,182,212,0.3)',
|
||||
color: '#f97316',
|
||||
radius: resultForZones.aeglDistances.aegl2,
|
||||
angle: meteo.windDirDeg,
|
||||
},
|
||||
{
|
||||
level: 'AEGL-1',
|
||||
color: 'rgba(6,182,212,0.25)',
|
||||
color: '#eab308',
|
||||
radius: resultForZones.aeglDistances.aegl1,
|
||||
angle: meteo.windDirDeg,
|
||||
},
|
||||
].filter((z: { radius: number }) => z.radius > 0),
|
||||
contours: resultForZones.contours,
|
||||
timestamp: new Date().toISOString(),
|
||||
windDirection: meteo.windDirDeg,
|
||||
substance: substanceName,
|
||||
@ -771,6 +819,10 @@ export function HNSView() {
|
||||
},
|
||||
maxConcentration: resultForZones.maxConcentration,
|
||||
});
|
||||
|
||||
// 지도 자동 줌 — 확산 영역 전체를 화면에 맞춤
|
||||
const bounds = computeBoundsFromHeatmap(resultForZones.points);
|
||||
if (bounds) setFitBoundsTarget(bounds);
|
||||
} catch (err) {
|
||||
console.error('[HNS] 분석 재계산 실패:', err);
|
||||
}
|
||||
@ -932,6 +984,7 @@ export function HNSView() {
|
||||
incidentCoord={incidentCoord ?? undefined}
|
||||
flyToIncident={flyToCoord}
|
||||
onIncidentFlyEnd={() => setFlyToCoord(undefined)}
|
||||
fitBoundsTarget={fitBoundsTarget}
|
||||
isSelectingLocation={isSelectingLocation}
|
||||
onMapClick={handleMapClick}
|
||||
oilTrajectory={[]}
|
||||
@ -1009,7 +1062,7 @@ export function HNSView() {
|
||||
// 좌측 패널 UI도 동기화
|
||||
setLoadedParams(merged);
|
||||
setInputParams(merged);
|
||||
// 병합된 파라미터로 즉시 예측 실행
|
||||
// 병합된 파라미터로 즉시 예측 실행 (handleRunPrediction 내부에서 fitBounds 자동 적용)
|
||||
handleRunPrediction(merged);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -19,6 +19,7 @@ import type {
|
||||
AeglDistances,
|
||||
AeglAreas,
|
||||
AlgorithmType,
|
||||
ContourLine,
|
||||
} from './dispersionTypes';
|
||||
import { getSubstanceToxicity } from './toxicityData';
|
||||
|
||||
@ -332,6 +333,7 @@ export function computeDispersion(params: ComputeDispersionParams): DispersionGr
|
||||
|
||||
// g/m³ → ppm 변환 + 지도 좌표 변환
|
||||
const points: DispersionPoint[] = [];
|
||||
const ppmGrid = new Float64Array(sim.ny * sim.nx);
|
||||
let maxConcentration = 0;
|
||||
|
||||
// AEGL 거리/면적 계산용
|
||||
@ -350,6 +352,7 @@ export function computeDispersion(params: ComputeDispersionParams): DispersionGr
|
||||
for (let i = 0; i < sim.nx; i++) {
|
||||
const idx = j * sim.nx + i;
|
||||
const ppm = gm3ToPpm(cGm3[idx], tox.mw, meteo.temperature, meteo.pressure);
|
||||
ppmGrid[idx] = ppm;
|
||||
|
||||
if (ppm < 0.01) continue;
|
||||
|
||||
@ -391,6 +394,29 @@ export function computeDispersion(params: ComputeDispersionParams): DispersionGr
|
||||
aegl3: parseFloat(((aegl3Cells * cellAreaM2) / 1e6).toFixed(2)),
|
||||
};
|
||||
|
||||
// AEGL 등농도선 추출 (marching squares)
|
||||
const contours: ContourLine[] = [];
|
||||
const aeglLevels: Array<{ level: ContourLine['level']; threshold: number; color: string }> = [
|
||||
{ level: 'AEGL-3', threshold: tox.aegl3, color: '#ef4444' },
|
||||
{ level: 'AEGL-2', threshold: tox.aegl2, color: '#f97316' },
|
||||
{ level: 'AEGL-1', threshold: tox.aegl1, color: '#eab308' },
|
||||
];
|
||||
for (const { level, threshold, color } of aeglLevels) {
|
||||
const segments = extractContourSegments(
|
||||
ppmGrid,
|
||||
sim.nx,
|
||||
sim.ny,
|
||||
xArr,
|
||||
yArr,
|
||||
originLon,
|
||||
originLat,
|
||||
threshold,
|
||||
);
|
||||
if (segments.length > 0) {
|
||||
contours.push({ level, threshold, color, segments });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
points,
|
||||
maxConcentration: parseFloat(maxConcentration.toFixed(2)),
|
||||
@ -399,5 +425,103 @@ export function computeDispersion(params: ComputeDispersionParams): DispersionGr
|
||||
modelType,
|
||||
timeStep: t,
|
||||
substance: substanceName,
|
||||
contours,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Marching squares로 스칼라 그리드에서 등농도선(iso-contour) 선분 추출
|
||||
* 각 2×2 셀의 4 코너 값이 threshold 이상인지 비트마스크(0~15)로 16 케이스 분기.
|
||||
* 결과는 위경도 좌표 선분 리스트.
|
||||
*/
|
||||
function extractContourSegments(
|
||||
ppmGrid: Float64Array,
|
||||
nx: number,
|
||||
ny: number,
|
||||
xArr: Float64Array,
|
||||
yArr: Float64Array,
|
||||
originLon: number,
|
||||
originLat: number,
|
||||
threshold: number,
|
||||
): Array<[[number, number], [number, number]]> {
|
||||
const segments: Array<[[number, number], [number, number]]> = [];
|
||||
if (threshold <= 0) return segments;
|
||||
|
||||
for (let j = 0; j < ny - 1; j++) {
|
||||
for (let i = 0; i < nx - 1; i++) {
|
||||
const bl = ppmGrid[j * nx + i];
|
||||
const br = ppmGrid[j * nx + (i + 1)];
|
||||
const tr = ppmGrid[(j + 1) * nx + (i + 1)];
|
||||
const tl = ppmGrid[(j + 1) * nx + i];
|
||||
|
||||
const mask =
|
||||
(bl > threshold ? 1 : 0) |
|
||||
(br > threshold ? 2 : 0) |
|
||||
(tr > threshold ? 4 : 0) |
|
||||
(tl > threshold ? 8 : 0);
|
||||
|
||||
if (mask === 0 || mask === 15) continue;
|
||||
|
||||
const x0 = xArr[i];
|
||||
const x1 = xArr[i + 1];
|
||||
const y0 = yArr[j];
|
||||
const y1 = yArr[j + 1];
|
||||
|
||||
const lerp = (a: number, b: number): number => {
|
||||
const denom = b - a;
|
||||
if (Math.abs(denom) < 1e-9) return 0.5;
|
||||
return Math.max(0, Math.min(1, (threshold - a) / denom));
|
||||
};
|
||||
|
||||
// 엣지 교차점 (미터 좌표)
|
||||
const mLeft: [number, number] = [x0, y0 + lerp(bl, tl) * (y1 - y0)];
|
||||
const mBottom: [number, number] = [x0 + lerp(bl, br) * (x1 - x0), y0];
|
||||
const mRight: [number, number] = [x1, y0 + lerp(br, tr) * (y1 - y0)];
|
||||
const mTop: [number, number] = [x0 + lerp(tl, tr) * (x1 - x0), y1];
|
||||
|
||||
const toLL = (p: [number, number]): [number, number] =>
|
||||
metersToLonLat(p[0], p[1], originLon, originLat);
|
||||
|
||||
const push = (a: [number, number], b: [number, number]) => {
|
||||
segments.push([toLL(a), toLL(b)]);
|
||||
};
|
||||
|
||||
switch (mask) {
|
||||
case 1:
|
||||
case 14:
|
||||
push(mLeft, mBottom);
|
||||
break;
|
||||
case 2:
|
||||
case 13:
|
||||
push(mBottom, mRight);
|
||||
break;
|
||||
case 3:
|
||||
case 12:
|
||||
push(mLeft, mRight);
|
||||
break;
|
||||
case 4:
|
||||
case 11:
|
||||
push(mRight, mTop);
|
||||
break;
|
||||
case 5:
|
||||
push(mLeft, mBottom);
|
||||
push(mRight, mTop);
|
||||
break;
|
||||
case 6:
|
||||
case 9:
|
||||
push(mBottom, mTop);
|
||||
break;
|
||||
case 7:
|
||||
case 8:
|
||||
push(mLeft, mTop);
|
||||
break;
|
||||
case 10:
|
||||
push(mLeft, mTop);
|
||||
push(mBottom, mRight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
@ -67,6 +67,15 @@ export interface AeglAreas {
|
||||
aegl3: number;
|
||||
}
|
||||
|
||||
/** AEGL 등농도선 (marching squares 결과) */
|
||||
export interface ContourLine {
|
||||
level: 'AEGL-1' | 'AEGL-2' | 'AEGL-3';
|
||||
threshold: number; // ppm
|
||||
color: string; // hex
|
||||
/** 선분 리스트 — 각 선분은 [lon,lat] 양끝점 */
|
||||
segments: Array<[[number, number], [number, number]]>;
|
||||
}
|
||||
|
||||
/** 확산 계산 전체 결과 */
|
||||
export interface DispersionGridResult {
|
||||
points: DispersionPoint[];
|
||||
@ -76,6 +85,7 @@ export interface DispersionGridResult {
|
||||
modelType: DispersionModel;
|
||||
timeStep: number; // 현재 시간 (s)
|
||||
substance: string;
|
||||
contours?: ContourLine[];
|
||||
}
|
||||
|
||||
/** computeDispersion 입력 파라미터 */
|
||||
|
||||
@ -386,7 +386,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div
|
||||
style={{ fontSize: '22px', color: probColor, lineHeight: 1 }}
|
||||
style={{ fontSize: 'var(--font-size-heading-3)', color: probColor, lineHeight: 1 }}
|
||||
className="font-bold font-mono"
|
||||
>
|
||||
{vessel.probability}%
|
||||
|
||||
@ -53,10 +53,10 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: 'var(--fg-default)', fontSize: 14, fontWeight: 600 }}>
|
||||
<div style={{ color: 'var(--fg-default)', fontSize: 'var(--font-size-body-2)', fontWeight: 600 }}>
|
||||
확산 예측 실패
|
||||
</div>
|
||||
<div style={{ color: 'var(--fg-disabled)', fontSize: 12, marginTop: 2 }}>
|
||||
<div style={{ color: 'var(--fg-disabled)', fontSize: 'var(--font-size-caption)', marginTop: 2 }}>
|
||||
시뮬레이션 실행 중 오류가 발생했습니다
|
||||
</div>
|
||||
</div>
|
||||
@ -70,7 +70,7 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '10px 14px',
|
||||
color: 'rgb(252, 165, 165)',
|
||||
fontSize: 13,
|
||||
fontSize: 'var(--font-size-label-1)',
|
||||
lineHeight: 1.6,
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
@ -88,7 +88,7 @@ const SimulationErrorModal = ({ message, onClose }: SimulationErrorModalProps) =
|
||||
border: '1px solid rgba(239, 68, 68, 0.35)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: 'rgb(252, 165, 165)',
|
||||
fontSize: 13,
|
||||
fontSize: 'var(--font-size-label-1)',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.15s',
|
||||
|
||||
@ -56,10 +56,10 @@ const SimulationLoadingOverlay = ({ status, progress }: SimulationLoadingOverlay
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: 'var(--fg-default)', fontSize: 14, fontWeight: 600 }}>
|
||||
<div style={{ color: 'var(--fg-default)', fontSize: 'var(--font-size-body-2)', fontWeight: 600 }}>
|
||||
확산 예측 분석 중
|
||||
</div>
|
||||
<div style={{ color: 'var(--fg-disabled)', fontSize: 12, marginTop: 2 }}>
|
||||
<div style={{ color: 'var(--fg-disabled)', fontSize: 'var(--font-size-caption)', marginTop: 2 }}>
|
||||
{statusText}
|
||||
</div>
|
||||
</div>
|
||||
@ -92,10 +92,10 @@ const SimulationLoadingOverlay = ({ status, progress }: SimulationLoadingOverlay
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--fg-disabled)', fontSize: 11 }}>
|
||||
<span style={{ color: 'var(--fg-disabled)', fontSize: 'var(--font-size-caption)' }}>
|
||||
{status === 'PENDING' ? '대기 중' : '분석 진행 중'}
|
||||
</span>
|
||||
<span style={{ color: 'var(--color-accent)', fontSize: 12, fontWeight: 600 }}>
|
||||
<span style={{ color: 'var(--color-accent)', fontSize: 'var(--font-size-caption)', fontWeight: 600 }}>
|
||||
{status === 'PENDING' ? '—' : `${displayProgress}%`}
|
||||
</span>
|
||||
</div>
|
||||
@ -105,7 +105,7 @@ const SimulationLoadingOverlay = ({ status, progress }: SimulationLoadingOverlay
|
||||
<div
|
||||
style={{
|
||||
color: 'var(--fg-disabled)',
|
||||
fontSize: 11,
|
||||
fontSize: 'var(--font-size-caption)',
|
||||
lineHeight: 1.6,
|
||||
borderTop: '1px solid var(--stroke-light)',
|
||||
paddingTop: 12,
|
||||
|
||||
@ -7,6 +7,7 @@ import type { ScatSegment } from './scatTypes';
|
||||
import type { ApiZoneItem } from '../services/scatApi';
|
||||
import { esiColor } from './scatConstants';
|
||||
import { hexToRgba } from '@common/components/map/mapUtils';
|
||||
import { cn } from '@common/utils/cn';
|
||||
|
||||
interface ScatMapProps {
|
||||
segments: ScatSegment[];
|
||||
@ -105,6 +106,10 @@ function ScatMap({
|
||||
}: ScatMapProps) {
|
||||
const [zoom, setZoom] = useState(10);
|
||||
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
|
||||
const [collapsed, setCollapsed] = useState<{ esi: boolean; progress: boolean }>({
|
||||
esi: false,
|
||||
progress: false,
|
||||
});
|
||||
|
||||
// zones 첫 렌더 기준으로 초기 중심 좌표 결정 (이후 불변)
|
||||
const [initialCenter] = useState<[number, number]>(() =>
|
||||
@ -268,7 +273,7 @@ function ScatMap({
|
||||
)}
|
||||
|
||||
{/* Status chips */}
|
||||
<div className="absolute top-3.5 left-3.5 flex gap-2 z-[1000]">
|
||||
<div className="absolute top-3.5 left-3.5 flex gap-2 z-20">
|
||||
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-[color-mix(in_srgb,var(--bg-base)_92%,transparent)] backdrop-blur-xl border border-stroke rounded-full text-label-2 text-fg-sub font-korean">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-color-accent shadow-[0_0_6px_var(--color-accent)]" />
|
||||
Pre-SCAT 사전조사
|
||||
@ -279,63 +284,92 @@ function ScatMap({
|
||||
</div>
|
||||
|
||||
{/* Right info cards */}
|
||||
<div className="absolute top-3.5 right-8 w-[260px] flex flex-col gap-2 z-[1000] max-h-[calc(100%-100px)] overflow-y-auto scrollbar-thin">
|
||||
<div className="absolute top-3.5 right-8 w-[260px] flex flex-col gap-2 z-20 max-h-[calc(100%-100px)] overflow-y-auto scrollbar-thin">
|
||||
{/* ESI Legend */}
|
||||
<div className="bg-[color-mix(in_srgb,var(--bg-base)_92%,transparent)] backdrop-blur-xl border border-stroke rounded-md p-3.5 shadow-[0_4px_20px_rgba(0,0,0,0.3)]">
|
||||
<div className="text-caption font-bold uppercase tracking-wider text-fg-disabled mb-2.5">
|
||||
ESI 민감도 분류 범례
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between text-caption font-bold uppercase tracking-wider text-fg-disabled',
|
||||
!collapsed.esi && 'mb-2.5',
|
||||
)}
|
||||
>
|
||||
<span className="font-korean">ESI 민감도 분류 범례</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCollapsed((s) => ({ ...s, esi: !s.esi }))}
|
||||
aria-label={collapsed.esi ? '펼치기' : '접기'}
|
||||
className="w-5 h-5 flex items-center justify-center text-fg-sub hover:text-fg transition-colors"
|
||||
>
|
||||
{collapsed.esi ? '+' : '−'}
|
||||
</button>
|
||||
</div>
|
||||
{[
|
||||
{ esi: 'ESI 10', label: '갯벌·습지·맹그로브', color: '#991b1b' },
|
||||
{ esi: 'ESI 9', label: '쉘터 갯벌', color: '#b91c1c' },
|
||||
{ esi: 'ESI 8', label: '쉘터 암반 해안', color: '#dc2626' },
|
||||
{ esi: 'ESI 7', label: '노출 갯벌', color: '#ef4444' },
|
||||
{ esi: 'ESI 6', label: '자갈·혼합 해안', color: '#f97316' },
|
||||
{ esi: 'ESI 5', label: '혼합 모래/자갈', color: '#fb923c' },
|
||||
{ esi: 'ESI 3-4', label: '모래 해안', color: '#facc15' },
|
||||
{ esi: 'ESI 1-2', label: '암반·인공 구조물', color: '#4ade80' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2 py-1 text-label-2">
|
||||
<span
|
||||
className="w-3.5 h-1.5 rounded-sm flex-shrink-0"
|
||||
style={{ background: item.color }}
|
||||
/>
|
||||
<span className="text-fg-sub font-korean">{item.label}</span>
|
||||
<span className="ml-auto font-mono text-caption text-fg">{item.esi}</span>
|
||||
</div>
|
||||
))}
|
||||
{!collapsed.esi &&
|
||||
[
|
||||
{ esi: 'ESI 10', label: '갯벌·습지·맹그로브', color: '#991b1b' },
|
||||
{ esi: 'ESI 9', label: '쉘터 갯벌', color: '#b91c1c' },
|
||||
{ esi: 'ESI 8', label: '쉘터 암반 해안', color: '#dc2626' },
|
||||
{ esi: 'ESI 7', label: '노출 갯벌', color: '#ef4444' },
|
||||
{ esi: 'ESI 6', label: '자갈·혼합 해안', color: '#f97316' },
|
||||
{ esi: 'ESI 5', label: '혼합 모래/자갈', color: '#fb923c' },
|
||||
{ esi: 'ESI 3-4', label: '모래 해안', color: '#facc15' },
|
||||
{ esi: 'ESI 1-2', label: '암반·인공 구조물', color: '#4ade80' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2 py-1 text-label-2">
|
||||
<span
|
||||
className="w-3.5 h-1.5 rounded-sm flex-shrink-0"
|
||||
style={{ background: item.color }}
|
||||
/>
|
||||
<span className="text-fg-sub font-korean">{item.label}</span>
|
||||
<span className="ml-auto font-mono text-caption text-fg">{item.esi}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="bg-[color-mix(in_srgb,var(--bg-base)_92%,transparent)] backdrop-blur-xl border border-stroke rounded-md p-3.5 shadow-[0_4px_20px_rgba(0,0,0,0.3)]">
|
||||
<div className="text-caption font-bold uppercase tracking-wider text-fg-disabled mb-2.5">
|
||||
조사 정보
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between text-caption font-bold uppercase tracking-wider text-fg-disabled',
|
||||
!collapsed.progress && 'mb-2.5',
|
||||
)}
|
||||
>
|
||||
<span className="font-korean">조사 정보</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCollapsed((s) => ({ ...s, progress: !s.progress }))}
|
||||
aria-label={collapsed.progress ? '펼치기' : '접기'}
|
||||
className="w-5 h-5 flex items-center justify-center text-fg-sub hover:text-fg transition-colors"
|
||||
>
|
||||
{collapsed.progress ? '+' : '−'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2.5">
|
||||
{[
|
||||
['총 해안선', `${(totalLen / 1000).toFixed(1)} km`, ''],
|
||||
['조사 완료', `${(doneLen / 1000).toFixed(1)} km`, 'var(--color-success)'],
|
||||
['고민감 구간', `${(highSens / 1000).toFixed(1)} km`, 'var(--color-danger)'],
|
||||
[
|
||||
'방제 우선 구간',
|
||||
`${segments.filter((s) => s.sensitivity === '최상').length}개`,
|
||||
'var(--color-warning)',
|
||||
],
|
||||
].map(([label, val, color], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 border-b border-stroke last:border-b-0 text-label-2"
|
||||
>
|
||||
<span className="text-fg-sub font-korean">{label}</span>
|
||||
<span
|
||||
className="font-mono font-medium text-label-2"
|
||||
style={{ color: color || undefined }}
|
||||
{!collapsed.progress && (
|
||||
<div className="mt-2.5">
|
||||
{[
|
||||
['총 해안선', `${(totalLen / 1000).toFixed(1)} km`, ''],
|
||||
['조사 완료', `${(doneLen / 1000).toFixed(1)} km`, 'var(--color-success)'],
|
||||
['고민감 구간', `${(highSens / 1000).toFixed(1)} km`, 'var(--color-danger)'],
|
||||
[
|
||||
'방제 우선 구간',
|
||||
`${segments.filter((s) => s.sensitivity === '최상').length}개`,
|
||||
'var(--color-warning)',
|
||||
],
|
||||
].map(([label, val, color], i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between py-1.5 border-b border-stroke last:border-b-0 text-label-2"
|
||||
>
|
||||
{val}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-fg-sub font-korean">{label}</span>
|
||||
<span
|
||||
className="font-mono font-medium text-label-2"
|
||||
style={{ color: color || undefined }}
|
||||
>
|
||||
{val}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -20,19 +20,6 @@ export default defineConfig({
|
||||
'/scat/img': {
|
||||
target: process.env.VITE_SERVER_URL || 'https://wing-demo.gc-si.dev/',
|
||||
changeOrigin: true,
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq, req) => {
|
||||
console.log(
|
||||
`[scat-img] ${req.url} → ${proxyReq.protocol}//${proxyReq.host}${proxyReq.path}`,
|
||||
);
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req) => {
|
||||
console.log(`[scat-img] ${req.url} ← ${proxyRes.statusCode}`);
|
||||
});
|
||||
proxy.on('error', (err, req) => {
|
||||
console.error(`[scat-img] ${req.url} ERROR:`, err.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user