diff --git a/package.json b/package.json index c32839c..73bc4dd 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", "react-router-dom": "^7.8.1", + "recharts": "^3.1.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwind-scrollbar-hide": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98fd303..a37b2fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: react-router-dom: specifier: ^7.8.1 version: 7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + recharts: + specifier: ^3.1.2 + version: 3.1.2(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react-is@16.13.1)(react@19.1.1)(redux@5.0.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -675,6 +678,17 @@ packages: '@types/react': optional: true + '@reduxjs/toolkit@2.8.2': + resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/pluginutils@1.0.0-beta.30': resolution: {integrity: sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==} @@ -781,6 +795,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -985,6 +1002,33 @@ packages: '@tybys/wasm-util@0.10.0': resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1005,6 +1049,9 @@ packages: '@types/react@19.1.10': resolution: {integrity: sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.39.1': resolution: {integrity: sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1346,6 +1393,50 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1378,6 +1469,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1451,6 +1545,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + esbuild@0.25.9: resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} @@ -1781,6 +1878,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1793,6 +1893,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2332,6 +2436,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-router-dom@7.8.1: resolution: {integrity: sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==} engines: {node: '>=20.0.0'} @@ -2353,6 +2469,22 @@ packages: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} + recharts@3.1.2: + resolution: {integrity: sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2361,6 +2493,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2563,6 +2698,9 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2636,6 +2774,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@7.1.2: resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3155,6 +3296,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.10 + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.1.1 + react-redux: 9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1) + '@rolldown/pluginutils@1.0.0-beta.30': {} '@rollup/rollup-android-arm-eabi@4.46.2': @@ -3219,6 +3372,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} '@swc/core-darwin-arm64@1.13.3': @@ -3376,6 +3531,30 @@ snapshots: tslib: 2.8.1 optional: true + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3394,6 +3573,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.39.1(@typescript-eslint/parser@8.39.1(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3756,6 +3937,44 @@ snapshots: csstype@3.1.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -3784,6 +4003,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3924,6 +4145,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.39.10: {} + esbuild@0.25.9: optionalDependencies: '@esbuild/aix-ppc64': 0.25.9 @@ -4319,6 +4542,8 @@ snapshots: ignore@7.0.5: {} + immer@10.1.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4332,6 +4557,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -4787,6 +5014,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.1.1 + use-sync-external-store: 1.5.0(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + redux: 5.0.1 + react-router-dom@7.8.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -4803,6 +5039,32 @@ snapshots: react@19.1.1: {} + recharts@3.1.2(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react-is@16.13.1)(react@19.1.1)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1))(react@19.1.1) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.39.10 + eventemitter3: 5.0.1 + immer: 10.1.1 + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.1.10)(react@19.1.1)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.5.0(react@19.1.1) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4823,6 +5085,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -5082,6 +5346,8 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + tiny-invariant@1.3.3: {} + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -5198,6 +5464,23 @@ snapshots: dependencies: react: 19.1.1 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@7.1.2(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1): dependencies: esbuild: 0.25.9 diff --git a/src/features/main/components/features/chart/ChartBox.tsx b/src/features/main/components/features/chart/ChartBox.tsx new file mode 100644 index 0000000..225d833 --- /dev/null +++ b/src/features/main/components/features/chart/ChartBox.tsx @@ -0,0 +1,129 @@ +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +import { getChartColor, getChartColorClass } from '@/shared'; + +type DataPoint = { + year: string; + [key: string]: string | number; +}; + +type Props = { + data: DataPoint[]; + lines: { + key: string; + name: string; + showDot?: boolean; + }[]; + title?: string; + height?: number; + yAxisFormatter?: (value: number) => string; + tooltipFormatter?: (value: number, name: string) => [string, string]; + labelFormatter?: (label: string) => string; +}; + +export const ChartBox = ({ + data, + lines, + title, + yAxisFormatter = (value) => value.toFixed(2), + tooltipFormatter = (value, name) => [`${value}%`, name], + labelFormatter = (label) => `20${label}년`, +}: Props) => { + return ( +
+ {title && ( +

{title}

+ )} + + + {/* 가로선 - 실선 */} + + {/* 세로선 - 점선 */} + + + + + { + if (!payload || payload.length === 0) return null; + + // lines 배열의 순서대로 범례를 생성 + return ( +
    + {lines.map((line, index) => ( +
  • +
    + {line.name} +
  • + ))} +
+ ); + }} + /> + {lines.map((line, index) => { + const color = getChartColor(index); + return ( + + ); + })} +
+
+
+ ); +}; diff --git a/src/features/main/components/features/chart/index.ts b/src/features/main/components/features/chart/index.ts new file mode 100644 index 0000000..a08fef3 --- /dev/null +++ b/src/features/main/components/features/chart/index.ts @@ -0,0 +1 @@ +export * from './ChartBox'; diff --git a/src/features/main/components/features/index.ts b/src/features/main/components/features/index.ts index 698d687..13bd4e4 100644 --- a/src/features/main/components/features/index.ts +++ b/src/features/main/components/features/index.ts @@ -1 +1,2 @@ export * from './form'; +export * from './chart'; diff --git a/src/features/main/mock/chart-data.mock.ts b/src/features/main/mock/chart-data.mock.ts new file mode 100644 index 0000000..f42fc2d --- /dev/null +++ b/src/features/main/mock/chart-data.mock.ts @@ -0,0 +1,84 @@ +// 임의의 데이터 생성 (2020-2025년) + +// 최근 6개월 가격 지수 변동률 + +const CHART_DATA = { + // 최근 6개월 가격 지수 변동률 + RECENT_PRICE_FLUCTUATIONS: [ + { year: '20', 기준치: 0, 변동률: 2.5 }, + { year: '21', 기준치: 0, 변동률: 1.8 }, + { year: '22', 기준치: 0, 변동률: 0.9 }, + { year: '23', 기준치: 0, 변동률: 3.2 }, + { year: '24', 기준치: 0, 변동률: 1.5 }, + { year: '25', 기준치: 0, 변동률: 2.1 }, + ], + // LTV 전세금, 매매가 그래프 + REAL_ESTATE_PRICE_DATA: [ + { year: '20', 전세금: 2.8, 매매가: 3.2, 임대료: 1.5 }, + { year: '21', 전세금: 3.1, 매매가: 3.5, 임대료: 1.8 }, + { year: '22', 전세금: 2.5, 매매가: 2.9, 임대료: 1.2 }, + { year: '23', 전세금: 3.8, 매매가: 4.1, 임대료: 2.1 }, + { year: '24', 전세금: 3.0, 매매가: 3.3, 임대료: 1.6 }, + { year: '25', 전세금: 3.4, 매매가: 3.7, 임대료: 1.9 }, + ], + // 해당 지역 대위변제 발생 빈도 및 증가율 + REGIONAL_INCREASE_RATE: [ + { year: '20', 증가율: 1.2 }, + { year: '21', 증가율: 1.8 }, + { year: '22', 증가율: 0.9 }, + { year: '23', 증가율: 2.5 }, + { year: '24', 증가율: 1.6 }, + { year: '25', 증가율: 2.1 }, + ], +}; + +export const CHART_DATA_CONFIGS = [ + { + title: '최근 6개월 가격 지수 변동률', + data: CHART_DATA.RECENT_PRICE_FLUCTUATIONS, + lines: [ + { + key: '기준치', + name: '기준치', + showDot: false, + }, + { + key: '변동률', + name: '변동률', + showDot: true, + }, + ], + }, + { + title: 'LTV 전세금, 매매가 그래프', + data: CHART_DATA.REAL_ESTATE_PRICE_DATA, + lines: [ + { + key: '전세금', + name: '전세금', + showDot: true, + }, + { + key: '매매가', + name: '매매가', + showDot: true, + }, + { + key: '임대료', + name: '임대료', + showDot: true, + }, + ], + }, + { + title: '해당 지역 대위변제 발생 빈도 및 증가율', + data: CHART_DATA.REGIONAL_INCREASE_RATE, + lines: [ + { + key: '증가율', + name: '증가율', + showDot: true, + }, + ], + }, +]; diff --git a/src/features/main/mock/index.ts b/src/features/main/mock/index.ts new file mode 100644 index 0000000..67bf2e5 --- /dev/null +++ b/src/features/main/mock/index.ts @@ -0,0 +1 @@ +export * from './chart-data.mock'; diff --git a/src/features/main/ui/ChartSection.tsx b/src/features/main/ui/ChartSection.tsx new file mode 100644 index 0000000..d9698b2 --- /dev/null +++ b/src/features/main/ui/ChartSection.tsx @@ -0,0 +1,16 @@ +import { ChartBox } from '../components'; +import { CHART_DATA_CONFIGS } from '../mock'; + +export const ChartSection = () => { + return ( +
+
+ {CHART_DATA_CONFIGS.map((config) => ( +
+ +
+ ))} +
+
+ ); +}; diff --git a/src/features/main/ui/index.ts b/src/features/main/ui/index.ts index a97fff7..724d6dc 100644 --- a/src/features/main/ui/index.ts +++ b/src/features/main/ui/index.ts @@ -1,2 +1,3 @@ +export * from './ChartSection'; export * from './InputSection'; export * from './MapSection'; diff --git a/src/index.css b/src/index.css index fa71aa4..15854c6 100644 --- a/src/index.css +++ b/src/index.css @@ -13,6 +13,14 @@ @custom-variant dark (&:is(.dark *)); @theme inline { + --color-chart-blue: var(--chart-blue); + --color-chart-orange: var(--chart-orange); + --color-chart-green: var(--chart-green); + --color-chart-purple: var(--chart-purple); + --color-chart-red: var(--chart-red); + --color-chart-yellow: var(--chart-yellow); + --color-chart-pink: var(--chart-pink); + --color-chart-indigo: var(--chart-indigo); --color-kakao: var(--kakao-color); --color-naver: var(--naver-color); --height-header: var(--layout-header-height); @@ -64,6 +72,14 @@ } :root { + --chart-blue: #0c3165; + --chart-orange: #f57a0c; + --chart-green: #10b981; + --chart-purple: #8b5cf6; + --chart-red: #ef4444; + --chart-yellow: #f59e0b; + --chart-pink: #ec4899; + --chart-indigo: #6366f1; --kakao-color: #fee502; --naver-color: #04c73c; --lighthouse-button-secondary: #f2f4f6; diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index 74eec76..c56903f 100644 --- a/src/pages/main/MainPage.tsx +++ b/src/pages/main/MainPage.tsx @@ -1,10 +1,15 @@ -import { InputSection, MapSection } from '@/features'; +import { ChartSection, InputSection, MapSection } from '@/features'; export default function MainPage() { return ( -
- - +
+
+ + +
+
+ +
); } diff --git a/src/shared/utils/chart/chart-colors.ts b/src/shared/utils/chart/chart-colors.ts new file mode 100644 index 0000000..dcc5dcd --- /dev/null +++ b/src/shared/utils/chart/chart-colors.ts @@ -0,0 +1,50 @@ +/** + * 차트 색상 관련 유틸리티 + */ + +// CSS 변수 기반 색상 배열 +const CHART_COLORS = [ + 'var(--chart-orange)', // 첫 번째 (1개일 때) + 'var(--chart-blue)', // 두 번째 + 'var(--chart-green)', // 세 번째 + 'var(--chart-purple)', // 네 번째 + 'var(--chart-red)', // 다섯 번째 + 'var(--chart-yellow)', // 여섯 번째 + 'var(--chart-pink)', // 일곱 번째 + 'var(--chart-indigo)', // 여덟 번째 +] as const; + +// Tailwind CSS 클래스 배열 +const CHART_COLOR_CLASSES = [ + 'bg-chart-orange', // 첫 번째 (1개일 때) + 'bg-chart-blue', // 두 번째 + 'bg-chart-green', // 세 번째 + 'bg-chart-purple', // 네 번째 + 'bg-chart-red', // 다섯 번째 + 'bg-chart-yellow', // 여섯 번째 + 'bg-chart-pink', // 일곱 번째 + 'bg-chart-indigo', // 여덟 번째 +] as const; + +/** + * 인덱스에 따른 차트 색상 CSS 변수를 반환 + * @param index - 색상 인덱스 (0부터 시작) + * @returns CSS 변수 문자열 + */ +export const getChartColor = (index: number): string => { + return CHART_COLORS[index % CHART_COLORS.length]; +}; + +/** + * 인덱스에 따른 차트 색상 Tailwind CSS 클래스를 반환 + * @param index - 색상 인덱스 (0부터 시작) + * @returns Tailwind CSS 클래스명 + */ +export const getChartColorClass = (index: number): string => { + return CHART_COLOR_CLASSES[index % CHART_COLOR_CLASSES.length]; +}; + +/** + * 차트 색상 개수 + */ +export const CHART_COLOR_COUNT = CHART_COLORS.length; diff --git a/src/shared/utils/chart/index.ts b/src/shared/utils/chart/index.ts new file mode 100644 index 0000000..e616679 --- /dev/null +++ b/src/shared/utils/chart/index.ts @@ -0,0 +1 @@ +export * from './chart-colors'; diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts index d99ca8c..3932761 100644 --- a/src/shared/utils/index.ts +++ b/src/shared/utils/index.ts @@ -1,3 +1,4 @@ export * from './scroll-to-top'; export * from './class-value'; export * from './image-cache'; +export * from './chart';