diff --git a/package-lock.json b/package-lock.json index 7d3dd3c..f46cd7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", "react-router-dom": "^7.8.2", + "recharts": "^3.2.0", "tailwindcss": "^4.1.12", "wx-react-gantt": "^1.3.1", "zustand": "^5.0.8" @@ -1610,6 +1611,32 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "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 + } + } + }, "node_modules/@restart/hooks": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", @@ -1889,6 +1916,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -2291,6 +2330,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz", + "integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2712,6 +2814,127 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/date-arithmetic": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", @@ -2741,6 +2964,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2873,6 +3102,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -3114,6 +3353,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3438,6 +3683,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3464,6 +3719,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4493,12 +4757,54 @@ "react-dom": ">=16.6.0" } }, + "node_modules/recharts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz", + "integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "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" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4834,6 +5140,46 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/victory-vendor/node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/vite": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", diff --git a/package.json b/package.json index 8fd8bd9..6fa3b9f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-dom": "^18.3.1", "react-error-boundary": "^6.0.0", "react-router-dom": "^7.8.2", + "recharts": "^3.2.0", "tailwindcss": "^4.1.12", "wx-react-gantt": "^1.3.1", "zustand": "^5.0.8" diff --git a/src/api/dashboardAPI.js b/src/api/dashboardAPI.js new file mode 100644 index 0000000..fdaca59 --- /dev/null +++ b/src/api/dashboardAPI.js @@ -0,0 +1,45 @@ +import api from './client.js'; + +/** + * 대시보드 프로젝트 리스트를 가져오는 API 함수 + * @returns {Promise} 프로젝트 데이터가 포함된 응답 객체 + */ +export const fetchDashboardProjects = async () => { + try { + const response = await api.get('/dashboards'); + return response.data; + } catch (error) { + console.error('대시보드 프로젝트 데이터 로딩 실패:', error); + throw error; + } +}; + +// /** +// * 특정 프로젝트의 상세 정보를 가져오는 API 함수 +// * @param {number} projectId - 프로젝트 ID +// * @returns {Promise} 프로젝트 상세 데이터 +// */ +// export const fetchProjectDetail = async (projectId) => { +// try { +// const response = await api.get(`/projects/${projectId}`); +// return response.data; +// } catch (error) { +// console.error('프로젝트 상세 데이터 로딩 실패:', error); +// throw error; +// } +// }; + +/** + * 특정 프로젝트의 대시보드 데이터를 가져오는 API 함수 + * @param {number} projectId - 프로젝트 ID + * @returns {Promise} 프로젝트 대시보드 데이터 (차트, 일정, 우선순위 등) + */ +export const fetchProjectDashboard = async (projectId) => { + try { + const response = await api.get(`/dashboards/${projectId}`); + return response.data; + } catch (error) { + console.error('프로젝트 대시보드 데이터 로딩 실패:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/components/common/ConfirmDialog.jsx b/src/components/common/ConfirmDialog.jsx new file mode 100644 index 0000000..a6fe987 --- /dev/null +++ b/src/components/common/ConfirmDialog.jsx @@ -0,0 +1,53 @@ +const ConfirmDialog = ({ + isOpen, + title, + message, + confirmText = "확인", + cancelText = "취소", + onConfirm, + onCancel, + type = "warning", +}) => { + if (!isOpen) return null; + + return ( + <> + {/* 다이얼로그 */} +
+
+ {/* 헤더 */} +
+

{title}

+
+ + {/* 메시지 */} +
+

{message}

+
+ + {/* 버튼들 */} +
+ + +
+
+
+ + ); +}; + +export default ConfirmDialog; \ No newline at end of file diff --git a/src/components/common/loading/LoadingComponents.jsx b/src/components/common/loading/LoadingComponents.jsx index 9239675..62dc85c 100644 --- a/src/components/common/loading/LoadingComponents.jsx +++ b/src/components/common/loading/LoadingComponents.jsx @@ -1,173 +1,70 @@ import { Replay, Warning} from "@mui/icons-material"; +import { Skeleton } from "@mui/material"; -// 기본 Skeleton 컴포넌트 - 개선된 애니메이션 -export function SkeletonBox({ className = "", width = "w-full", height = "h-4" }) { +// ProjectList용 Skeleton UI +export function ProjectListSkeleton({ count = 4 }) { return ( -
- ); -} +
+ {/* 헤더 스켈레톤 */} +
+
+ + +
+
-// 회의 테이블용 Skeleton UI -export function MeetingTableSkeleton({ rows = 10 }) { - // 제목 컬럼용 다양한 너비 배열 - const titleWidths = ['w-48', 'w-56', 'w-44', 'w-52', 'w-40']; - - return ( -
-
- - {/* 헤더 */} - - - - - - - - - - - - {/* 스켈레톤 로우들 */} - - {Array.from({ length: rows }).map((_, index) => ( - - {/* 제목 컬럼 */} - - - {/* 작성자 컬럼 */} - - - {/* 타입 컬럼 */} - - - {/* 진행 날짜 컬럼 */} - - - {/* 장소 컬럼 */} - - - {/* 참여 인원 컬럼 */} - - - ))} - -
- 제목 - - 작성자 - - 타입 - - 진행 날짜 - - 장소 - - 참여 인원 -
-
- -
- -
-
-
-
- - -
-
- - -
- -
- - -
+ {/* 프로젝트 목록 스켈레톤 */} +
+ {Array.from({ length: count }).map((_, index) => ( +
+ {/* 프로젝트 헤더 */} +
+
+ +
+
+ +
-
- - -
- - +
+ +
-
-
-
- ); -} +
+
+ + -// 범용 테이블 Skeleton UI (다른 테이블에서도 사용 가능) -export function TableSkeleton({ - rows = 5, - columns = 4, - showHeader = true, - headerTitles = [] -}) { - const defaultWidths = ['w-32', 'w-24', 'w-20', 'w-28', 'w-16']; - - return ( -
-
- - {/* 헤더 */} - {showHeader && ( - - - {Array.from({ length: columns }).map((_, colIndex) => ( - - ))} - - - )} - - {/* 스켈레톤 로우들 */} - - {Array.from({ length: rows }).map((_, rowIndex) => ( - - {Array.from({ length: columns }).map((_, colIndex) => ( - - ))} - - ))} - -
- {headerTitles[colIndex] || ( - - )} -
- -
+ {/* 진행률 스켈레톤 */} +
+
+ + +
+ +
+ + {/* 마감일 및 팀원 스켈레톤 */} +
+
+ + + +
+
+ + + +
+
+
+ ))}
); } + export function LoadingSpinner({ message = "데이터를 불러오는 중..." }) { return (
@@ -180,7 +77,7 @@ export function LoadingSpinner({ message = "데이터를 불러오는 중..." }) // 에러 경계 컴포넌트 export function ErrorFallback({ error, onRetry }) { return ( -
+
diff --git a/src/components/dashboard/ErrorList.jsx b/src/components/dashboard/ErrorList.jsx new file mode 100644 index 0000000..6dd7000 --- /dev/null +++ b/src/components/dashboard/ErrorList.jsx @@ -0,0 +1,122 @@ +import { useQuery } from '@tanstack/react-query'; +import { fetchProjectDashboard } from '../../api/dashboardAPI'; +import { Warning, Person, AccessTime } from '@mui/icons-material'; + +function ErrorList({ selectedProjectId }) { + const { data: dashboardData, isLoading, error } = useQuery({ + queryKey: ['projectDashboard', selectedProjectId], + queryFn: () => fetchProjectDashboard(selectedProjectId), + enabled: !!selectedProjectId, + }); + + const priorities = dashboardData?.data?.priorities?.priority || []; + + // WARNING 우선순위만 필터링 (에러로 간주) + const errorTasks = priorities.find(p => p.priority === 'WARNING')?.priorityDataList || []; + + if (isLoading) { + return ( +
+

에러 발생

+
+
+
+
+ ); + } + + if (error || !selectedProjectId) { + return ( +
+

에러 발생

+
+ {!selectedProjectId ? '프로젝트를 선택해주세요' : '데이터를 불러올 수 없습니다'} +
+
+ ); + } + + return ( +
+
+ +

에러 발생

+ {errorTasks.length > 0 && ( + + {errorTasks.length} + + )} +
+ + {errorTasks.length > 0 ? ( +
+ {errorTasks.map((task) => ( +
+
+
+ +
+ +
+
+

+ {task.title} +

+ + 긴급 + +
+ +
+
+ + {task.writerName} +
+
+ + 즉시 처리 필요 +
+
+
+
+ + {/* 액션 버튼 */} +
+ + +
+
+ ))} +
+ ) : ( +
+
+ + + +
+

에러가 없습니다

+

모든 작업이 정상적으로 진행되고 있습니다

+
+ )} + + {/* 요약 정보 */} +
+
+ + 총 에러 작업 + + 0 ? 'text-red-600' : 'text-green-600'}`}> + {errorTasks.length}개 + +
+
+
+ ); +} + +export default ErrorList; \ No newline at end of file diff --git a/src/components/dashboard/GitHubActivity.jsx b/src/components/dashboard/GitHubActivity.jsx deleted file mode 100644 index d953546..0000000 --- a/src/components/dashboard/GitHubActivity.jsx +++ /dev/null @@ -1,93 +0,0 @@ -function GitHubActivity() { - - //예시 데이터 - const activities = [ - { - id: 1, - type: 'commit', - message: 'feat: 대시보드 컴포넌트 추가', - author: '김개발', - time: '2시간 전', - repo: 'sysone-frontend' - }, - { - id: 2, - type: 'pr', - message: 'fix: 사이드바 네비게이션 버그 수정', - author: '박코딩', - time: '4시간 전', - repo: 'sysone-frontend' - }, - { - id: 3, - type: 'issue', - message: '로그인 페이지 UI 개선 요청', - author: '이디자인', - time: '6시간 전', - repo: 'sysone-frontend' - }, - { - id: 4, - type: 'commit', - message: 'refactor: API 호출 로직 개선', - author: '최백엔드', - time: '8시간 전', - repo: 'sysone-backend' - } - ]; - - const getTypeIcon = (type) => { - switch(type) { - case 'commit': return '💾'; - case 'pr': return '🔀'; - case 'issue': return '🐛'; - default: return '📝'; - } - }; - - const getTypeColor = (type) => { - switch(type) { - case 'commit': return 'bg-green-100 text-green-800'; - case 'pr': return 'bg-blue-100 text-blue-800'; - case 'issue': return 'bg-red-100 text-red-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - - return ( -
-
-

최근 GitHub 활동

- -
- -
- {activities.map((activity) => ( -
-
- {getTypeIcon(activity.type)} -
-
-
- - {activity.type.toUpperCase()} - - {activity.repo} -
-

{activity.message}

-
- {activity.author} - - {activity.time} -
-
-
- ))} -
-
- ) -} - -export default GitHubActivity diff --git a/src/components/dashboard/PriorityTasks.jsx b/src/components/dashboard/PriorityTasks.jsx index fc36002..a975a69 100644 --- a/src/components/dashboard/PriorityTasks.jsx +++ b/src/components/dashboard/PriorityTasks.jsx @@ -1,84 +1,154 @@ -function PriorityTasks() { - // 예시 데이터 - const tasks = { - high: [ - { id: 1, title: '로그인 API 연동', assignee: '김개발' }, - { id: 2, title: '보안 취약점 수정', assignee: '박보안' } - ], - medium: [ - { id: 3, title: '대시보드 차트 구현', assignee: '이프론트' }, - { id: 4, title: '사용자 권한 관리', assignee: '최백엔드' } - ], - low: [ - { id: 5, title: 'UI 다크모드 추가', assignee: '김디자인' }, - { id: 6, title: '문서화 작업', assignee: '박문서' } - ] - }; +import { useQuery } from '@tanstack/react-query'; +import { fetchProjectDashboard } from '../../api/dashboardAPI'; - const getPriorityColor = (priority) => { - switch(priority) { - case 'high': return 'border-red-200 bg-red-50'; - case 'medium': return 'border-yellow-200 bg-yellow-50'; - case 'low': return 'border-green-200 bg-green-50'; - default: return 'border-gray-200 bg-gray-50'; - } - }; +function PriorityTasks({ selectedProjectId }) { + const { data: dashboardData, isLoading, error } = useQuery({ + queryKey: ['projectDashboard', selectedProjectId], + queryFn: () => fetchProjectDashboard(selectedProjectId), + enabled: !!selectedProjectId, + }); + + const priorities = dashboardData?.data?.priorities?.priority || []; - const getPriorityLabel = (priority) => { - switch(priority) { - case 'high': return '🔴 높음'; - case 'medium': return '🟡 보통'; - case 'low': return '🟢 낮음'; - default: return '⚪ 기타'; + // 우선순위별 색상 매핑 + const getPriorityConfig = (priority) => { + switch (priority) { + case 'LOW': + return { + dotColor: 'bg-green-500', + label: '낮음', + bgColor: 'bg-gray-50' + }; + case 'NORMAL': + return { + dotColor: 'bg-yellow-500', + label: '보통', + bgColor: 'bg-gray-50' + }; + case 'HIGH': + return { + dotColor: 'bg-red-500', + label: '높음', + bgColor: 'bg-gray-50' + }; + case 'WARNING': + return { + dotColor: 'bg-red-600', + label: '긴급', + bgColor: 'bg-gray-50' + }; + default: + return { + dotColor: 'bg-gray-500', + label: priority, + bgColor: 'bg-gray-50' + }; } }; + if (isLoading) { + return ( +
+
+

우선순위 작업

+ +
+
+
+
+
+ ); + } + + if (error || !selectedProjectId) { + return ( +
+
+

우선순위 작업

+ +
+
+ {!selectedProjectId ? '프로젝트를 선택해주세요' : '데이터를 불러올 수 없습니다'} +
+
+ ); + } + + // WARNING 우선순위는 제외 (에러 리스트 컴포넌트에서 표시) + const filteredPriorities = priorities.filter(p => p.priority !== 'WARNING'); + return (
-
+

우선순위 작업

-
-
- {Object.entries(tasks).map(([priority, taskList]) => ( -
-
- - {getPriorityLabel(priority)} - - - ({taskList.length}개) - -
-
- {taskList.map((task) => ( -
-
- - {task.title} - -
-
- - {task.assignee.charAt(0)} - +
+ {filteredPriorities.map((priorityGroup) => { + const config = getPriorityConfig(priorityGroup.priority); + + return ( +
+ {/* 우선순위 헤더 */} +
+
+ + {config.label} + + + ({priorityGroup.priorityDataList.length}개) + +
+ + {/* 작업 목록 */} +
+ {priorityGroup.priorityDataList.slice(0, 3).map((task) => ( +
+
+
+
+ {task.title} +
+
+ {task.writerName} + + {task.writerName?.charAt(0)} + +
-
- ))} + ))} + + {/* 더 많은 항목이 있을 경우 */} + {priorityGroup.priorityDataList.length > 3 && ( +
+ +{priorityGroup.priorityDataList.length - 3}개 더 +
+ )} +
+
+ ); + })} + + {filteredPriorities.length === 0 && ( +
+
+
+

우선순위 작업이 없습니다

- ))} + )}
- ) + ); } -export default PriorityTasks +export default PriorityTasks; diff --git a/src/components/dashboard/ProjectList.jsx b/src/components/dashboard/ProjectList.jsx new file mode 100644 index 0000000..387ef35 --- /dev/null +++ b/src/components/dashboard/ProjectList.jsx @@ -0,0 +1,214 @@ +import { Suspense, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { ErrorBoundary } from 'react-error-boundary'; +import { + Group, + Assignment, + AccessTime, +} from '@mui/icons-material'; +import { fetchDashboardProjects } from '../../api/dashboardAPI'; +import { ProjectListSkeleton, ErrorFallback } from '../common/loading/LoadingComponents'; +import useProjectStore from '../../store/projectStore'; + +// 데이터 로딩 컴포넌트 (TanStack Query 사용) +function ProjectListContent() { + const navigate = useNavigate(); + const { selectedProjectId, setSelectedProjectId } = useProjectStore(); + + const { data: response} = useQuery({ + queryKey: ['projectList'], + queryFn: fetchDashboardProjects, + suspense: true, // Suspense 모드 활성화 + }); + + // API 응답에서 프로젝트 데이터 추출 + const projects = useMemo(() => response?.data?.projects || [], [response?.data?.projects]); + + // 첫 번째 프로젝트를 기본 선택으로 설정 + useEffect(() => { + if (projects.length > 0 && !selectedProjectId) { + setSelectedProjectId(projects[0].id); + } + }, [projects, selectedProjectId, setSelectedProjectId]); + + const getProgressColor = (progress) => { + if (progress >= 80) return '#10b981'; // green-500 + if (progress >= 50) return '#3b82f6'; // blue-500 + if (progress >= 30) return '#f59e0b'; // amber-500 + return '#9ca3af'; // gray-400 + }; + + const getStatusInfo = (daysRemaining, overdue) => { + if (overdue) return { text: '지연', color: 'text-red-600', bg: 'bg-red-50' }; + if (daysRemaining <= 3) return { text: '긴급', color: 'text-red-600', bg: 'bg-red-50' }; + if (daysRemaining <= 7) return { text: '임박', color: 'text-orange-600', bg: 'bg-orange-50' }; + return { text: '정상', color: 'text-green-600', bg: 'bg-green-50' }; + }; + + const isOverdue = (endDate) => { + return new Date(endDate) < new Date(); + }; + + const getDaysRemaining = (endDate) => { + const today = new Date(); + const end = new Date(endDate); + const diffTime = end - today; + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + }; + + const handleProjectClick = (projectId) => { + setSelectedProjectId(projectId); + // 선택한 프로젝트로 대시보드 데이터 업데이트 + }; + + const handleProjectDetailClick = (projectId, e) => { + e.stopPropagation(); // 프로젝트 선택 이벤트 방지 + navigate(`/projects/${projectId}`); + }; + + return ( +
+ {/* 헤더 */} +
+
+

내 프로젝트

+ {projects.length}개 +
+
+ + {/* 프로젝트 목록 */} +
+ {projects.map((project) => { + const daysRemaining = getDaysRemaining(project.endDate); + const overdue = isOverdue(project.endDate); + const status = getStatusInfo(daysRemaining, overdue); + const isSelected = selectedProjectId === project.id; + + return ( +
handleProjectClick(project.id)} + className={`p-4 cursor-pointer transition-colors group ${ + isSelected + ? 'bg-gray-100' + : 'hover:bg-gray-50' + }`} + > + {/* 프로젝트 헤더 */} +
+
+

+ {project.name} +

+
+ + + {project.participant.length}명 + + + + {project.totalTasks}개 작업 + +
+
+
+ + {status.text} + +
+
+ + {/* 진행률 */} +
+
+ 진행률 + + {Math.round(project.progressRate)}% + +
+
+
+
+
+ + {/* 마감일 및 팀원 */} +
+
+ + + {overdue ? '마감 지남' : `${daysRemaining}일 남음`} + + / + {new Date(project.endDate).toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' })} +
+
+ {project.participant.slice(0, 3).map((member, index) => ( + + {member.charAt(0)} + + ))} + {project.participant.length > 3 && ( + + +{project.participant.length - 3} + + )} + +
+
+
+ ); + })} +
+ + {/* 빈 상태 */} + {projects.length === 0 && ( +
+ +

참여중인 프로젝트가 없습니다

+
+ )} +
+ ); +} + +function ProjectList() { + const queryClient = useQueryClient(); + + return ( + ( + + )} + onReset={() => { + // 특정 쿼리만 무효화하여 재시도 (전체 새로고침 X) + queryClient.invalidateQueries({ queryKey: ['projectList'] }); + }} + > + }> + + + + ); +} + +export default ProjectList; \ No newline at end of file diff --git a/src/components/dashboard/ProjectProgressChart.jsx b/src/components/dashboard/ProjectProgressChart.jsx index e115a1a..452d160 100644 --- a/src/components/dashboard/ProjectProgressChart.jsx +++ b/src/components/dashboard/ProjectProgressChart.jsx @@ -1,41 +1,142 @@ -function ProjectProgressChart() { +import React from 'react'; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts'; +import { useQuery } from '@tanstack/react-query'; +import { fetchProjectDashboard } from '../../api/dashboardAPI'; - //예시 데이터 - const progressData = [ - { label: '완료', value: 45, color: '#10B981' }, - { label: '진행중', value: 35, color: '#3B82F6' }, - { label: '계획', value: 20, color: '#6B7280' } - ]; +function ProjectProgressChart({ selectedProjectId }) { + const { data: dashboardData, isLoading, error } = useQuery({ + queryKey: ['projectDashboard', selectedProjectId], + queryFn: () => fetchProjectDashboard(selectedProjectId), + enabled: !!selectedProjectId, + }); + + const projectGraph = dashboardData?.data?.projectGraph; + + // 디버깅을 위한 콘솔 로그 + React.useEffect(() => { + if (projectGraph) { + console.log('ProjectGraph 데이터:', projectGraph); + } + }, [projectGraph]); + + const pieData = React.useMemo(() => { + if (!projectGraph) return []; + + const data = []; + + if (projectGraph.todo > 0 || projectGraph.TODO > 0) { + data.push({ + name: '할 일', + value: Number(projectGraph.todo || projectGraph.TODO || 0), + color: '#6B7280' + }); + } + + if (projectGraph.progress > 0 || projectGraph.IN_PROGRESS > 0) { + data.push({ + name: '진행중', + value: Number(projectGraph.progress || projectGraph.IN_PROGRESS || 0), + color: '#3B82F6' + }); + } + + if (projectGraph.review > 0 || projectGraph.REVIEW > 0) { + data.push({ + name: '검토', + value: Number(projectGraph.review || projectGraph.REVIEW || 0), + color: '#F59E0B' + }); + } + + + if (projectGraph.done > 0 || projectGraph.DONE > 0) { + data.push({ + name: '완료', + value: Number(projectGraph.done || projectGraph.DONE || 0), + color: '#10B981' + }); + } + + console.log('Pie 차트 데이터:', data); + return data; + }, [projectGraph]); + + if (isLoading) { + return ( +
+

프로젝트 진행률

+
+
+
+
+ ); + } + + if (error || !selectedProjectId) { + return ( +
+

프로젝트 진행률

+
+ {!selectedProjectId ? '프로젝트를 선택해주세요' : '데이터를 불러올 수 없습니다'} +
+
+ ); + } return (

프로젝트 진행률

- {/* 임시 도넛 차트 - 나중에 실제 차트 라이브러리로 교체 */} -
-
-
- 80% -
-
- -
- {progressData.map((item, index) => ( -
-
-
- {item.label} -
- {item.value}% +
+ {pieData && pieData.length > 0 ? ( + + + + {pieData.map((entry, index) => ( + + ))} + + [`${value}%`, name]} + contentStyle={{ + backgroundColor: '#f9fafb', + border: '1px solid #e5e7eb', + borderRadius: '8px' + }} + /> + + + + ) : ( +
+
+

데이터가 없습니다

+

이슈가 등록되면 차트가 표시됩니다

- ))} -
+
+ )} +
+ + {/* 총 이슈 개수 표시 */} +
+ + 총 {projectGraph?.total || 0}개 이슈 +
- ) + ); } export default ProjectProgressChart diff --git a/src/components/dashboard/RecentNotifications.jsx b/src/components/dashboard/RecentNotifications.jsx deleted file mode 100644 index bab31c3..0000000 --- a/src/components/dashboard/RecentNotifications.jsx +++ /dev/null @@ -1,120 +0,0 @@ -function RecentNotifications() { - - //예시 데이터 - const notifications = [ - { - id: 1, - type: 'mention', - title: '김팀장님이 회의록에서 언급했습니다', - message: '"UI 개선 건은 이번 주까지 완료해주세요"', - time: '5분 전', - isRead: false - }, - { - id: 2, - type: 'approval', - title: '프로젝트 예산 승인 요청', - message: '개발팀 추가 장비 구매 건이 승인 대기 중입니다', - time: '1시간 전', - isRead: false - }, - { - id: 3, - type: 'update', - title: '프로젝트 상태 업데이트', - message: '모바일 앱 개발이 80% 완료되었습니다', - time: '3시간 전', - isRead: true - }, - { - id: 4, - type: 'system', - title: '시스템 점검 안내', - message: '오늘 밤 12시-2시 서버 점검이 예정되어 있습니다', - time: '5시간 전', - isRead: true - } - ]; - - const getNotificationIcon = (type) => { - switch(type) { - case 'mention': return '💬'; - case 'approval': return '✅'; - case 'update': return '📈'; - case 'system': return '⚙️'; - default: return '📢'; - } - }; - - const getNotificationColor = (type) => { - switch(type) { - case 'mention': return 'bg-blue-100 text-blue-800'; - case 'approval': return 'bg-yellow-100 text-yellow-800'; - case 'update': return 'bg-green-100 text-green-800'; - case 'system': return 'bg-gray-100 text-gray-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - - return ( -
-
-

최근 알림 & 활동

-
- - {notifications.filter(n => !n.isRead).length} - - -
-
- -
- {notifications.map((notification) => ( -
-
-
- {getNotificationIcon(notification.type)} -
-
-
- - {notification.type.toUpperCase()} - - {!notification.isRead && ( -
- )} -
-

- {notification.title} -

-

- {notification.message} -

- - {notification.time} - -
-
-
- ))} -
- -
- -
-
- ) -} - -export default RecentNotifications diff --git a/src/components/dashboard/TeamProductivityTrend.jsx b/src/components/dashboard/TeamProductivityTrend.jsx index b0b24dd..b61bcf6 100644 --- a/src/components/dashboard/TeamProductivityTrend.jsx +++ b/src/components/dashboard/TeamProductivityTrend.jsx @@ -1,71 +1,133 @@ -import React from 'react' +import React from 'react'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { useQuery } from '@tanstack/react-query'; +import { fetchProjectDashboard } from '../../api/dashboardAPI'; -function TeamProductivityTrend() { - //예시 데이터 - const weeklyData = [ - { week: '1주차', completed: 85, planned: 90 }, - { week: '2주차', completed: 92, planned: 95 }, - { week: '3주차', completed: 78, planned: 85 }, - { week: '4주차', completed: 88, planned: 90 } - ]; +function TeamProductivityTrend({ selectedProjectId }) { + const { data: dashboardData, isLoading, error } = useQuery({ + queryKey: ['projectDashboard', selectedProjectId], + queryFn: () => fetchProjectDashboard(selectedProjectId), + enabled: !!selectedProjectId, + }); + + // Bar 차트용 데이터 변환 + const barData = React.useMemo(() => { + const issuesGraph = dashboardData?.data?.issuesGraph || []; + if (!issuesGraph || issuesGraph.length === 0) return []; + + return issuesGraph.map(item => ({ + name: item.name || 'Unknown', + '할당 기간': item.allottedPeriod || 0, + '완료 기간': item.completedPeriod || 0, + nameColor: (item.completedPeriod || 0) > (item.allottedPeriod || 0) ? '#EF4444' : '#10B981' // 지연시 빨강, 정상시 초록 + })); + }, [dashboardData]); + + if (isLoading) { + return ( +
+

인원별 이슈 진행률

+
+
+
+
+ ); + } + + if (error || !selectedProjectId) { + return ( +
+

인원별 이슈 진행률

+
+ {!selectedProjectId ? '프로젝트를 선택해주세요' : '데이터를 불러올 수 없습니다'} +
+
+ ); + } return (
-

팀 생산성 트렌드

+

인원별 이슈 진행률

- {/* 임시 차트 - 나중에 실제 차트 라이브러리로 교체 */} -
-
-
-
- 완료율 -
-
-
- 계획 대비 +
+ {barData.length > 0 ? ( + + + + + + [ + `${value}일`, + name + ]} + /> + + + + + + ) : ( +
+
+

데이터가 없습니다

+

이슈가 할당되면 차트가 표시됩니다

+
-
+ )} +
-
- {weeklyData.map((data, index) => ( -
-
- {data.week} - - {data.completed}% - -
-
-
- {/* 계획 대비 진행률 (회색 배경) */} -
- {/* 실제 완료율 (파란색) */} -
-
-
+ {/* 범례 및 상태 정보 */} +
+
+
+
+
+ 할당 기간 +
+
+
+ 완료 기간
- ))} -
- - {/* 요약 정보 */} -
-
- 평균 완료율 - 85.8% -
-
- 지난 주 대비 +3.2% 상승
+ + {/* 지연 알림 */} + {barData.some(item => item['완료 기간'] > item['할당 기간']) && ( +
+ ⚠️ 일부 작업이 예정 기간을 초과했습니다. +
+ )}
- ) + ); } export default TeamProductivityTrend diff --git a/src/components/dashboard/WeeklySchedule.jsx b/src/components/dashboard/WeeklySchedule.jsx index e08c3c3..d480b67 100644 --- a/src/components/dashboard/WeeklySchedule.jsx +++ b/src/components/dashboard/WeeklySchedule.jsx @@ -1,16 +1,61 @@ -import React from 'react' +import { useQuery } from '@tanstack/react-query'; +import { fetchProjectDashboard } from '../../api/dashboardAPI'; -function WeeklySchedule() { +function WeeklySchedule({ selectedProjectId }) { + const { data: dashboardData, isLoading, error } = useQuery({ + queryKey: ['projectDashboard', selectedProjectId], + queryFn: () => fetchProjectDashboard(selectedProjectId), + enabled: !!selectedProjectId, + }); - //예시 데이터 - const today = new Date(); - const weekDays = ['월', '화', '수', '목', '금', '토', '일']; + const weekendIssues = dashboardData?.data?.weekendIssues?.weekendIssue || {}; - const scheduleItems = [ - { time: '09:00', title: '팀 스탠드업 미팅', type: 'meeting' }, - { time: '14:00', title: 'UI 디자인 리뷰', type: 'review' }, - { time: '16:30', title: '클라이언트 미팅', type: 'meeting' }, - ]; + const weekDays = ['월', '화', '수', '목', '금', '토', '일']; + const today = new Date(); + const currentDay = today.getDay() === 0 ? 7 : today.getDay(); // 일요일을 7로 변환 + + // 상태별 색상 매핑 + const getStatusColor = (status) => { + switch (status) { + case 'TODO': return 'bg-gray-400'; + case 'PROGRESS': return 'bg-blue-400'; + case 'REVIEW': return 'bg-orange-400'; + case 'DONE': return 'bg-green-400'; + default: return 'bg-gray-400'; + } + }; + + const getStatusText = (status) => { + switch (status) { + case 'TODO': return '할 일'; + case 'PROGRESS': return '진행중'; + case 'REVIEW': return '검토'; + case 'DONE': return '완료'; + default: return status; + } + }; + + if (isLoading) { + return ( +
+

이번 주 일정

+
+
+
+
+ ); + } + + if (error || !selectedProjectId) { + return ( +
+

이번 주 일정

+
+ {!selectedProjectId ? '프로젝트를 선택해주세요' : '데이터를 불러올 수 없습니다'} +
+
+ ); + } return (
@@ -18,37 +63,70 @@ function WeeklySchedule() { {/* 미니 캘린더 */}
- {weekDays.map((day, index) => ( -
-
{day}
-
- {index + 1} + {weekDays.map((day, index) => { + const dayNumber = index + 1; + const dayIssues = weekendIssues[dayNumber] || []; + const isToday = dayNumber === currentDay; + + return ( +
+
{day}
+
+ {dayNumber} + {dayIssues.length > 0 && ( +
+ {dayIssues.length} +
+ )} +
-
- ))} + ); + })}
{/* 오늘 일정 */}
-

오늘 일정

- {scheduleItems.map((item, index) => ( -
-
-
-
{item.title}
-
{item.time}
-
+

+ 오늘 일정 ({weekDays[currentDay - 1]}) +

+ + {weekendIssues[currentDay] && weekendIssues[currentDay].length > 0 ? ( +
+ {weekendIssues[currentDay].map((issue) => ( +
+
+
+
{issue.title}
+
+ {new Date(issue.date).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })} + + {getStatusText(issue.status)} + +
+
+
+ ))} +
+ ) : ( +
+ 오늘 예정된 이슈가 없습니다 +
+ )} + + {/* 주간 요약 */} +
+
이번 주 요약
+
+ 총 {Object.values(weekendIssues).flat().length}개 이슈 예정
- ))} +
- ) + ); } export default WeeklySchedule diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index 39c8e3b..6c6aca2 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -120,7 +120,7 @@ function Sidebar() {

{user?.name}

-

{user?.department}

+

{user?.position}

diff --git a/src/components/member/MemberManagement.jsx b/src/components/member/MemberManagement.jsx index 21b7d96..7611e3e 100644 --- a/src/components/member/MemberManagement.jsx +++ b/src/components/member/MemberManagement.jsx @@ -1,8 +1,10 @@ import { useState } from 'react'; import { useMemberQueries } from '../../hooks/useMemberQueries'; +import { useToast } from '../../hooks/useToast'; import MemberTable from './MemberTable'; import MemberModal from './MemberModal'; import MemberHeader from './MemberHeader'; +import ConfirmDialog from '../common/ConfirmDialog'; function MemberManagement() { const { @@ -17,8 +19,17 @@ function MemberManagement() { isDeleting } = useMemberQueries(); + const { showSuccess, showError } = useToast(); + const [isModalOpen, setIsModalOpen] = useState(false); const [editingMember, setEditingMember] = useState(null); + const [confirmDialog, setConfirmDialog] = useState({ + isOpen: false, + title: '', + message: '', + onConfirm: null, + type: 'warning' + }); const [formData, setFormData] = useState({ accountId: '', password: '', @@ -58,7 +69,7 @@ function MemberManagement() { setEditingMember(member); setFormData({ accountId: member.accountId, - password: '', // 수정 시에는 비밀번호 필드 비움 + password: '', role: member.role, email: member.email, name: member.name, @@ -69,15 +80,22 @@ function MemberManagement() { }; // 회원 삭제 - const handleDeleteMember = async (memberId) => { - if (window.confirm('정말로 이 회원을 삭제하시겠습니까?')) { - const result = await removeMember(memberId); - if (result.success) { - alert('회원이 성공적으로 삭제되었습니다.'); - } else { - alert('회원 삭제에 실패했습니다: ' + result.error); + const handleDeleteMember = async (member) => { + setConfirmDialog({ + isOpen: true, + title: '회원 삭제', + message: `정말로 ${member.name} 회원을 삭제하시겠습니까?\n삭제된 회원 정보는 복구할 수 없습니다.`, + type: 'danger', + onConfirm: async () => { + setConfirmDialog(prev => ({ ...prev, isOpen: false })); + const result = await removeMember(member.id); + if (result.success) { + showSuccess('회원이 성공적으로 삭제되었습니다.'); + } else { + showError('회원 삭제에 실패했습니다: ' + result.error); + } } - } + }); }; // 폼 제출 핸들러 (React Query의 뮤테이션 상태 활용) @@ -100,9 +118,9 @@ function MemberManagement() { if (result.success) { setIsModalOpen(false); - alert(editingMember ? '회원 정보가 수정되었습니다.' : '새 회원이 등록되었습니다.'); + showSuccess(editingMember ? '회원 정보가 수정되었습니다.' : '새 회원이 등록되었습니다.'); } else { - alert('작업에 실패했습니다: ' + result.error); + showError('작업에 실패했습니다: ' + result.error); } }; @@ -112,6 +130,11 @@ function MemberManagement() { setEditingMember(null); }; + // 확인 다이얼로그 닫기 + const handleCloseConfirmDialog = () => { + setConfirmDialog(prev => ({ ...prev, isOpen: false })); + }; + if (loading) { return (
@@ -151,6 +174,17 @@ function MemberManagement() { onSubmit={handleSubmit} isSubmitting={isAdding || isEditing} /> + +
); } diff --git a/src/components/member/MemberTable.jsx b/src/components/member/MemberTable.jsx index 9cd740b..ad9034b 100644 --- a/src/components/member/MemberTable.jsx +++ b/src/components/member/MemberTable.jsx @@ -67,7 +67,7 @@ function MemberTable({ members, onEdit, onDelete }) {
-

+

{project.description}

diff --git a/src/data/dashboardProjects.js b/src/data/dashboardProjects.js new file mode 100644 index 0000000..99d1f5b --- /dev/null +++ b/src/data/dashboardProjects.js @@ -0,0 +1,65 @@ +// 더미 데이터 - API 연동 전까지 사용 +export const dummyProjectData = { + statusCode: 200, + message: "프로젝트 리스트 호출 성공", + data: { + projectCount: 3, + issueCount: 8, + projects: [ + { + id: 1, + name: "채팅 애플리케이션 개발", + participant: ["김개발", "이디자인", "박기획"], + progressRate: 75.5, + totalTasks: 24, + endDate: "2025-09-25T23:59:59.000+00:00" + }, + { + id: 2, + name: "웹 어플리케이션 리뉴얼", + participant: ["최프론트", "한백엔드"], + progressRate: 45.2, + totalTasks: 18, + endDate: "2025-10-15T23:59:59.000+00:00" + }, + { + id: 3, + name: "모바일 앱 개발", + participant: ["송모바일", "윤테스트", "정데브옵스"], + progressRate: 30.8, + totalTasks: 32, + endDate: "2025-11-30T23:59:59.000+00:00" + }, + { + id: 4, + name: "AI 챗봇 시스템", + participant: ["이AI", "박머신러닝"], + progressRate: 60.3, + totalTasks: 15, + endDate: "2025-10-30T23:59:59.000+00:00" + } + ] + } +}; + +// API 호출 함수 (현재는 더미 데이터 반환) +export const fetchDashboardProjects = async () => { + try { + // TODO: 실제 API 엔드포인트로 변경 + // const response = await fetch('{{domain}}/api/dashboards'); + // if (!response.ok) { + // throw new Error('API 호출 실패'); + // } + // return await response.json(); + + // 현재는 더미 데이터 반환 (네트워크 지연 시뮬레이션) + return new Promise((resolve) => { + setTimeout(() => { + resolve(dummyProjectData); + }, 500); + }); + } catch (error) { + console.error('프로젝트 데이터 로딩 실패:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/hooks/useMeetingQueries.js b/src/hooks/useMeetingQueries.js index 7e1b491..8c83a67 100644 --- a/src/hooks/useMeetingQueries.js +++ b/src/hooks/useMeetingQueries.js @@ -6,8 +6,6 @@ export function useMeetings(projectId = 1, page = 1, size = 10) { return useQuery({ queryKey: ['meetings', projectId, page, size], queryFn: () => getMeetings(projectId, page, size), - staleTime: 2 * 60 * 1000, // 2분간 신선한 데이터 - cacheTime: 1 * 60 * 1000, // 10분간 캐시 유지 refetchOnWindowFocus: true, select: (response) => { if (response.statusCode === 200) { @@ -95,7 +93,6 @@ export function useMeetingDetail(projectId, meetingId, enabled = true) { queryKey: ['meeting-detail', projectId, meetingId], queryFn: () => getMeetingDetail(projectId, meetingId), enabled: enabled && !!meetingId, - staleTime: 5 * 60 * 1000, select: (response) => { if (response.statusCode === 200) { return { @@ -167,7 +164,6 @@ export function usePrefetchNextPage(projectId, currentPage, totalPages, size = 1 queryClient.prefetchQuery({ queryKey: ['meetings', projectId, currentPage + 1, size], queryFn: () => getMeetings(projectId, currentPage + 1, size), - staleTime: 2 * 60 * 1000 }); } }; diff --git a/src/hooks/useMemberQueries.js b/src/hooks/useMemberQueries.js index 0f7e13c..f62873d 100644 --- a/src/hooks/useMemberQueries.js +++ b/src/hooks/useMemberQueries.js @@ -27,8 +27,6 @@ export const useMemberQueries = () => { const response = await getMembers(); return response.data; }, - staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지 - gcTime: 10 * 60 * 1000, // 10분간 캐시 유지 }); const members = membersData || []; diff --git a/src/hooks/useToast.js b/src/hooks/useToast.js index 9cb1379..58a3e85 100644 --- a/src/hooks/useToast.js +++ b/src/hooks/useToast.js @@ -3,8 +3,5 @@ import ToastContext from '../contexts/ToastContext'; export const useToast = () => { const context = useContext(ToastContext); - if (!context) { - throw new Error('useToast must be used within a ToastProvider'); - } return context; }; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 88081e0..083408d 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -11,7 +11,7 @@ import { ToastProvider } from './contexts/ToastContext.jsx' const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 5 * 60 * 1000, // 5분 + // staleTime: 3 * 60 * 1000, retry: 1, }, }, diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 144c324..5205b7a 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,15 +1,18 @@ -import KPICard from '../components/dashboard/KPICard' -import ProjectProgressChart from '../components/dashboard/ProjectProgressChart' -import WeeklySchedule from '../components/dashboard/WeeklySchedule' -import GitHubActivity from '../components/dashboard/GitHubActivity' -import PriorityTasks from '../components/dashboard/PriorityTasks' -import TeamProductivityTrend from '../components/dashboard/TeamProductivityTrend' -import RecentNotifications from '../components/dashboard/RecentNotifications' -import { GitHub, People, Source, Warning } from '@mui/icons-material' -import { useNavigate } from 'react-router-dom' +import KPICard from "../components/dashboard/KPICard"; +import ProjectProgressChart from "../components/dashboard/ProjectProgressChart"; +import WeeklySchedule from "../components/dashboard/WeeklySchedule"; +import PriorityTasks from "../components/dashboard/PriorityTasks"; +import TeamProductivityTrend from "../components/dashboard/TeamProductivityTrend"; +import ErrorList from "../components/dashboard/ErrorList"; +import ProjectList from "../components/dashboard/ProjectList"; +import useProjectStore from "../store/projectStore"; +import { Source, Warning } from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; function Dashboard() { const navigate = useNavigate(); + const { selectedProjectId } = useProjectStore(); + const kpiData = [ { title: "진행 중인 프로젝트", @@ -17,66 +20,59 @@ function Dashboard() { icon: , trend: "up", path: "/projects", - trendValue: "+2 from last month" + trendValue: "+2 from last month", }, { title: "이번 주 마감 작업", value: "8", icon: , trend: "down", - trendValue: "-3 from last week" + trendValue: "-3 from last week", }, - { - title: "금주 GitHub 커밋", - value: "47", - icon: , - trend: "up", - trendValue: "+12 from last week" - }, - { - title: "활성 팀원", - value: "15", - icon: , - trend: "up", - trendValue: "+1 new member" - } ]; return (
- {/* KPI 카드들 */} -
- {kpiData.map((kpi, index) => ( - kpi.path && navigate(kpi.path)} - /> - ))} -
- - {/* 메인 위젯들 */} -
- {/* 왼쪽 컬럼 */} -
- - - +
+
+
+ {/* KPI 카드들 */} + +
+ + + +
+
+
+
+
+
+ {kpiData.map((kpi, index) => ( + kpi.path && navigate(kpi.path)} + /> + ))} +
+ + + +
- {/* 오른쪽 컬럼 */} -
- - - + {/* 오른쪽 1/4 영역 - 내 참여 프로젝트 */} +
+
- ) + ); } -export default Dashboard +export default Dashboard; diff --git a/src/store/projectStore.js b/src/store/projectStore.js new file mode 100644 index 0000000..68e1ca2 --- /dev/null +++ b/src/store/projectStore.js @@ -0,0 +1,9 @@ +import { create } from 'zustand'; + +// Zustand store로 프로젝트 선택 상태 관리 +const useProjectStore = create((set) => ({ + selectedProjectId: null, + setSelectedProjectId: (id) => set({ selectedProjectId: id }), +})); + +export default useProjectStore; \ No newline at end of file