From fede2eda2699c1b19600e4b6855669a7c098b547 Mon Sep 17 00:00:00 2001 From: sheetEasy AI Team Date: Wed, 2 Jul 2025 16:36:46 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Footer/TopBar=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EB=B0=8F=20=EB=AA=A8=EB=93=A0=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B0=9C=EC=84=A0,=20Suspense=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=8C=80=EC=9D=91,=20sheetEasyAI=20?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=9E=9C?= =?UTF-8?q?=EB=94=A9=20=EC=9D=B4=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 587 ++++++++++++++++++++++++++ package.json | 8 + src/App.tsx | 328 +++++++++----- src/components/AccountPage.tsx | 552 ++++++++++++++++++++++++ src/components/ContactPage.tsx | 417 ++++++++++++++++++ src/components/LandingPage.tsx | 24 +- src/components/LicensePage.tsx | 389 +++++++++++++++++ src/components/PrivacyPolicyPage.tsx | 410 ++++++++++++++++++ src/components/RoadmapPage.tsx | 258 +++++++++++ src/components/SupportPage.tsx | 380 +++++++++++++++++ src/components/TermsOfServicePage.tsx | 277 ++++++++++++ src/components/UpdatesPage.tsx | 244 +++++++++++ src/components/ui/alert-dialog.tsx | 141 +++++++ src/components/ui/badge.tsx | 36 ++ src/components/ui/dialog.tsx | 122 ++++++ src/components/ui/footer.tsx | 138 +++--- src/components/ui/historyPanel.tsx | 21 +- src/components/ui/progress.tsx | 28 ++ src/components/ui/select.tsx | 157 +++++++ src/components/ui/topbar.tsx | 22 +- src/lib/i18n.tsx | 367 ++++++++++++++++ src/stores/useAppStore.ts | 21 + 22 files changed, 4735 insertions(+), 192 deletions(-) create mode 100644 src/components/AccountPage.tsx create mode 100644 src/components/ContactPage.tsx create mode 100644 src/components/LicensePage.tsx create mode 100644 src/components/PrivacyPolicyPage.tsx create mode 100644 src/components/RoadmapPage.tsx create mode 100644 src/components/SupportPage.tsx create mode 100644 src/components/TermsOfServicePage.tsx create mode 100644 src/components/UpdatesPage.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/lib/i18n.tsx diff --git a/package-lock.json b/package-lock.json index 70b0553..5595ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,10 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-select": "^2.2.5", "@univerjs/core": "^0.8.2", "@univerjs/design": "^0.8.2", "@univerjs/docs": "^0.8.2", @@ -29,11 +33,15 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "file-saver": "^2.0.5", + "i18next": "^25.3.0", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.468.0", "luckyexcel": "^1.0.1", "luckysheet": "^2.1.13", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^15.5.3", + "recharts": "^3.0.2", "tailwind-merge": "^2.5.4", "zustand": "^5.0.2" }, @@ -1682,12 +1690,46 @@ "license": "BSD-3-Clause", "peer": true }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", + "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2143,6 +2185,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", @@ -2174,6 +2240,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", @@ -2334,6 +2443,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", @@ -2438,6 +2562,47 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "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/@reduxjs/toolkit/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/@reduxjs/toolkit/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/@rolldown/pluginutils": { "version": "1.0.0-beta.11", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", @@ -2752,6 +2917,18 @@ "@sinonjs/commons": "^3.0.0" } }, + "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/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -2930,6 +3107,69 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "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": "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/@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/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3134,6 +3374,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -7831,6 +8077,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": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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-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": "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-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -7876,6 +8243,12 @@ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "license": "MIT" }, + "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-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -8125,6 +8498,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.39.5", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.5.tgz", + "integrity": "sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -8425,6 +8808,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/expect": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.1.tgz", @@ -8995,6 +9384,15 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9024,6 +9422,46 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.0.tgz", + "integrity": "sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9073,6 +9511,16 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "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", @@ -9127,6 +9575,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "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/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -11459,6 +11916,32 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.3.tgz", + "integrity": "sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -11666,6 +12149,64 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.0.2.tgz", + "integrity": "sha512-eDc3ile9qJU9Dp/EekSthQPhAVPG48/uM47jk+PF7VBQngxeW3cwQpPHb/GHC1uqwyCRWXcIrDzuHRVrnRryoQ==", + "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/recharts/node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts/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", + "optional": true, + "peer": true + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -11715,6 +12256,12 @@ "dev": true, "license": "MIT" }, + "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/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -12835,6 +13382,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12850,6 +13406,28 @@ "uuid": "dist/bin/uuid" } }, + "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/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", @@ -13062,6 +13640,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vue": { "version": "3.5.17", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz", diff --git a/package.json b/package.json index 5198147..63a8144 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-select": "^2.2.5", "@univerjs/core": "^0.8.2", "@univerjs/design": "^0.8.2", "@univerjs/docs": "^0.8.2", @@ -38,11 +42,15 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "file-saver": "^2.0.5", + "i18next": "^25.3.0", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.468.0", "luckyexcel": "^1.0.1", "luckysheet": "^2.1.13", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^15.5.3", + "recharts": "^3.0.2", "tailwind-merge": "^2.5.4", "zustand": "^5.0.2" }, diff --git a/src/App.tsx b/src/App.tsx index f0ce46c..871fb3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,16 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "./components/ui/button"; import { TopBar } from "./components/ui/topbar"; import { lazy, Suspense } from "react"; import LandingPage from "./components/LandingPage"; import { SignUpPage } from "./components/auth/SignUpPage"; import { SignInPage } from "./components/auth/SignInPage"; +import AccountPage from "./components/AccountPage"; +import LicensePage from "./components/LicensePage"; import { useAppStore } from "./stores/useAppStore"; +import { I18nProvider } from "./lib/i18n"; import type { TutorialItem } from "./types/tutorial"; +import { startTransition } from "react"; // TutorialSheetViewer 동적 import const TutorialSheetViewer = lazy( @@ -18,6 +22,16 @@ const EditSheetViewer = lazy( () => import("./components/sheet/EditSheetViewer"), ); +// 새로운 페이지들 동적 import +const RoadmapPage = lazy(() => import("./components/RoadmapPage")); +const UpdatesPage = lazy(() => import("./components/UpdatesPage")); +const SupportPage = lazy(() => import("./components/SupportPage")); +const ContactPage = lazy(() => import("./components/ContactPage")); +const PrivacyPolicyPage = lazy(() => import("./components/PrivacyPolicyPage")); +const TermsOfServicePage = lazy( + () => import("./components/TermsOfServicePage"), +); + // 앱 상태 타입 정의 type AppView = | "landing" @@ -25,7 +39,14 @@ type AppView = | "signIn" | "editor" | "account" - | "tutorial"; + | "tutorial" + | "license" + | "roadmap" + | "updates" + | "support" + | "contact" + | "privacy-policy" + | "terms-of-service"; function App() { const [currentView, setCurrentView] = useState("landing"); @@ -36,8 +57,27 @@ function App() { user, currentFile, startTutorial, + setLanguage, + setHistoryPanelPosition, } = useAppStore(); + // 초기화: localStorage에서 설정 로드 + useEffect(() => { + // 언어 설정 로드 + const savedLanguage = localStorage.getItem("sheeteasy-language"); + if (savedLanguage === "ko" || savedLanguage === "en") { + setLanguage(savedLanguage); + } + + // 히스토리 패널 위치 설정 로드 + const savedPosition = localStorage.getItem( + "sheeteasy-history-panel-position", + ); + if (savedPosition === "left" || savedPosition === "right") { + setHistoryPanelPosition(savedPosition); + } + }, [setLanguage, setHistoryPanelPosition]); + // CTA 버튼 클릭 핸들러 - 인증 상태에 따른 분기 처리 const handleGetStarted = () => { if (isAuthenticated) { @@ -134,6 +174,45 @@ function App() { }, 100); }; + // Footer 핸들러들 - 새로운 페이지 네비게이션 + const handleRoadmapClick = () => { + startTransition(() => { + setCurrentView("roadmap"); + }); + }; + const handleUpdatesClick = () => { + startTransition(() => { + setCurrentView("updates"); + }); + }; + const handleSupportClick = () => { + if (isAuthenticated) { + startTransition(() => { + setCurrentView("support"); + }); + } else { + alert("지원 서비스는 로그인 후 이용 가능합니다."); + startTransition(() => { + setCurrentView("signIn"); + }); + } + }; + const handleContactClick = () => { + startTransition(() => { + setCurrentView("contact"); + }); + }; + const handlePrivacyPolicyClick = () => { + startTransition(() => { + setCurrentView("privacy-policy"); + }); + }; + const handleTermsOfServiceClick = () => { + startTransition(() => { + setCurrentView("terms-of-service"); + }); + }; + // 튜토리얼 선택 핸들러 - 튜토리얼 시작 후 튜토리얼 페이지로 전환 const handleTutorialSelect = (tutorial: TutorialItem) => { console.log("🎯 튜토리얼 선택됨:", tutorial.metadata.title); @@ -225,6 +304,11 @@ function App() { const handleGoToSignIn = () => setCurrentView("signIn"); const handleGoToSignUp = () => setCurrentView("signUp"); const handleGoToEditor = () => setCurrentView("editor"); + const handleGoToLicense = () => { + startTransition(() => { + setCurrentView("license"); + }); + }; // 에디터에서 홈으로 돌아가기 핸들러 (워닝 포함) const handleEditorLogoClick = () => { @@ -290,110 +374,27 @@ function App() { showAuthButtons={false} onAccountClick={handleAccountClick} /> -
-
-

계정 정보

+
+ +
+
+ ); - {/* 사용자 정보 */} -
-

사용자 정보

-
-

- 이메일: {user?.email} -

-

- 이름: {user?.name} -

-

- 가입일:{" "} - {user?.createdAt?.toLocaleDateString()} -

-

- 최근 로그인:{" "} - {user?.lastLoginAt?.toLocaleDateString()} -

-
-
- - {/* 구독 정보 */} -
-

구독 정보

-
-

- 플랜:{" "} - {user?.subscription?.plan?.toUpperCase()} -

-

- 상태: {user?.subscription?.status} -

-

- 다음 결제일:{" "} - {user?.subscription?.currentPeriodEnd?.toLocaleDateString()} -

-
-
- - {/* 사용량 */} -
-

사용량

-
-

- AI 쿼리:{" "} - {user?.subscription?.usage?.aiQueries} /{" "} - {user?.subscription?.plan === "free" - ? "30" - : user?.subscription?.plan === "lite" - ? "100" - : "500"} -

-

- 셀 카운트:{" "} - {user?.subscription?.usage?.cellCount} /{" "} - {user?.subscription?.plan === "free" - ? "300" - : user?.subscription?.plan === "lite" - ? "1000" - : "5000"} -

-
-
- - {/* 설정 */} -
-

설정

-
-

- 언어:{" "} - {user?.preferences?.language === "ko" - ? "한국어" - : "English"} -

-

- 히스토리 패널 위치:{" "} - {user?.preferences?.historyPanelPosition === "right" - ? "우측" - : "좌측"} -

-
-
- - {/* 액션 버튼들 */} -
- - -
- + case "license": + return ( +
+ +
+
); @@ -464,6 +465,108 @@ function App() { ); + case "roadmap": + return ( +
+ +
+ +
+
+ ); + + case "updates": + return ( +
+ +
+ +
+
+ ); + + case "support": + return ( +
+ +
+ +
+
+ ); + + case "contact": + return ( +
+ +
+ +
+
+ ); + + case "privacy-policy": + return ( +
+ +
+ +
+
+ ); + + case "terms-of-service": + return ( +
+ +
+ +
+
+ ); + case "landing": default: return ( @@ -473,6 +576,7 @@ function App() { showAccount={false} showNavigation={true} showAuthButtons={true} + showTestAccount={true} onSignInClick={handleGoToSignIn} onGetStartedClick={handleGetStarted} onAccountClick={handleAccountClick} @@ -481,6 +585,7 @@ function App() { onFeaturesClick={handleFeaturesClick} onFAQClick={handleFAQClick} onPricingClick={handlePricingClick} + onLogoClick={handleBackToLanding} /> ); } }; - return
{renderCurrentView()}
; + return ( + +
{renderCurrentView()}
+
+ ); } export default App; diff --git a/src/components/AccountPage.tsx b/src/components/AccountPage.tsx new file mode 100644 index 0000000..5a45a9e --- /dev/null +++ b/src/components/AccountPage.tsx @@ -0,0 +1,552 @@ +import * as React from "react"; +import { useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/card"; +import { Button } from "./ui/button"; +import { Badge } from "./ui/badge"; +import { Progress } from "./ui/progress"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "./ui/alert-dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { useAppStore } from "../stores/useAppStore"; +import { useTranslation, formatDate } from "../lib/i18n.tsx"; +import type { User } from "../types/user"; +import type { Language } from "../lib/i18n.tsx"; + +interface AccountPageProps { + onGoToEditor?: () => void; + onLogout?: () => void; +} + +/** + * 현대적인 shadcn UI를 사용한 Account 페이지 컴포넌트 + * - 구독 플랜 관리 (Card, Dialog, AlertDialog) + * - 사용량 분석 차트 (recharts) + * - 사용량 한계 경고 (Badge, Progress) + * - 사용자 설정 + */ +const AccountPage: React.FC = ({ + onGoToEditor, + onLogout, +}) => { + const { + user, + language, + historyPanelPosition, + setLanguage, + setHistoryPanelPosition, + } = useAppStore(); + const { t } = useTranslation(); + const [changePlanOpen, setChangePlanOpen] = useState(false); + const [cancelSubOpen, setCancelSubOpen] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + + // 플랜별 한계값 계산 + const getPlanLimits = (plan: string) => { + switch (plan) { + case "free": + return { aiQueries: 30, cellCount: 300 }; + case "lite": + return { aiQueries: 100, cellCount: 1000 }; + case "pro": + return { aiQueries: 500, cellCount: 5000 }; + default: + return { aiQueries: 0, cellCount: 0 }; + } + }; + + // 플랜 상태 배지 색상 + const getStatusBadgeVariant = (status: string) => { + switch (status) { + case "active": + return "default"; + case "trial": + return "secondary"; + case "canceled": + return "destructive"; + default: + return "outline"; + } + }; + + // 사용량 경고 여부 확인 + const isUsageWarning = (used: number, limit: number) => { + return used / limit >= 0.8; // 80% 이상 사용 시 경고 + }; + + // 사용량 진행률 계산 + const getUsagePercentage = (used: number, limit: number) => { + return Math.min((used / limit) * 100, 100); + }; + + // 확장된 사용량 데이터 (최근 30일) + const generateUsageData = () => { + const data = []; + const today = new Date(); + for (let i = 29; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + data.push({ + date: formatDate(date, language), + aiQueries: Math.floor(Math.random() * 10) + 1, + cellCount: Math.floor(Math.random() * 50) + 10, + promptCount: Math.floor(Math.random() * 8) + 1, + editedCells: Math.floor(Math.random() * 40) + 5, + }); + } + return data; + }; + + const usageData = generateUsageData(); + const limits = getPlanLimits(user?.subscription?.plan || "free"); + + if (!user) { + return ( +
+
+

{t.common.loading}

+
+
+ ); + } + + return ( +
+ {/* 페이지 헤더 */} +
+
+

+ {t.account.title} +

+

{t.account.subtitle}

+
+
+ + +
+
+ +
+ {/* 사용자 정보 카드 */} + + + {t.account.userInfo} + {t.account.userInfo} + + +
+

{t.account.email}

+

{user.email}

+
+
+

{t.account.name}

+

{user.name}

+
+
+

{t.account.joinDate}

+

+ {user.createdAt && formatDate(user.createdAt, language)} +

+
+
+

{t.account.lastLogin}

+

+ {user.lastLoginAt && formatDate(user.lastLoginAt, language)} +

+
+
+
+ + {/* 구독 플랜 카드 */} + + + + 구독 플랜 + + {user.subscription?.status?.toUpperCase()} + + + 현재 구독 정보 + + +
+

+ {user.subscription?.plan?.toUpperCase()} 플랜 +

+

+ 다음 결제일:{" "} + {user.subscription?.currentPeriodEnd?.toLocaleDateString( + "ko-KR", + )} +

+
+
+ + + + + + + 플랜 변경 + + 새로운 플랜을 선택하세요. 변경 사항은 즉시 적용됩니다. + + +
+
+ + + Free + + +

₩0

+

+
+
+ + + Lite + + +

₩5,900

+

+
+
+ + + Pro + + +

₩14,900

+

+
+
+
+
+ + + + +
+
+ + + + + + + + + 구독을 취소하시겠습니까? + + + 구독을 취소하면 현재 결제 주기가 끝날 때까지 서비스를 + 이용할 수 있습니다. 취소 후에는 Free 플랜으로 자동 + 전환됩니다. + + + + 취소 + 구독 취소 + + + +
+
+
+ + {/* 사용량 요약 카드 */} + + + 사용량 요약 + 현재 사용량 및 한계 + + + {/* AI 쿼리 사용량 */} +
+
+

AI 쿼리

+ {isUsageWarning( + user.subscription?.usage?.aiQueries || 0, + limits.aiQueries, + ) && ( + + 경고 + + )} +
+ +

+ {user.subscription?.usage?.aiQueries || 0} / {limits.aiQueries}{" "} + 사용 +

+
+ + {/* 셀 카운트 사용량 */} +
+
+

셀 카운트

+ {isUsageWarning( + user.subscription?.usage?.cellCount || 0, + limits.cellCount, + ) && ( + + 경고 + + )} +
+ +

+ {user.subscription?.usage?.cellCount || 0} / {limits.cellCount}{" "} + 사용 +

+
+
+
+
+ + {/* 사용량 분석 차트 */} + + + {t.account.usageAnalytics} + + {t.account.usageAnalyticsDescription} + + + +
+ + + + + + [ + value, + t.account.tooltipLabels[ + name as keyof typeof t.account.tooltipLabels + ] || name, + ]} + labelFormatter={(label) => `${t.account.date}: ${label}`} + /> + + + + + + +
+
+
+ + {/* 설정 카드 */} + + + {t.account.settings} + {t.account.settings} + + +
+
+

{t.account.language}

+ + {language === "ko" ? t.account.korean : t.account.english} + +
+
+

+ {t.account.historyPanelPosition} +

+ + {historyPanelPosition === "right" + ? t.account.right + : t.account.left} + +
+
+
+ + + + + + + {t.account.changeSettings} + {t.account.settings} + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + + +
+
+
+
+
+
+ ); +}; + +export default AccountPage; diff --git a/src/components/ContactPage.tsx b/src/components/ContactPage.tsx new file mode 100644 index 0000000..97dc16e --- /dev/null +++ b/src/components/ContactPage.tsx @@ -0,0 +1,417 @@ +import * as React from "react"; +import { useState } from "react"; +import { Button } from "./ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { cn } from "../lib/utils"; + +interface ContactPageProps { + className?: string; + onBack?: () => void; +} + +const ContactPage = React.forwardRef( + ({ className, onBack, ...props }, ref) => { + const [formData, setFormData] = useState({ + name: "", + email: "", + company: "", + subject: "", + message: "", + contactReason: "", + }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const contactReasons = [ + { value: "sales", label: "영업 문의", icon: "💼" }, + { value: "partnership", label: "파트너십", icon: "🤝" }, + { value: "feedback", label: "피드백", icon: "💭" }, + { value: "media", label: "미디어 문의", icon: "📰" }, + { value: "general", label: "일반 문의", icon: "💬" }, + { value: "other", label: "기타", icon: "❓" }, + ]; + + const handleInputChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + try { + const emailData = { + to: "contact@sheeteasy.ai", + from: formData.email, + subject: `[${formData.contactReason.toUpperCase()}] ${formData.subject}`, + html: ` +

문의사항

+

이름: ${formData.name}

+

이메일: ${formData.email}

+

회사: ${formData.company || "개인"}

+

문의 유형: ${contactReasons.find((r) => r.value === formData.contactReason)?.label}

+

제목: ${formData.subject}

+
+

내용:

+

${formData.message.replace(/\n/g, "
")}

+ `, + }; + + // TODO: 실제 이메일 서비스 연동 + console.log("문의사항 전송:", emailData); + + setTimeout(() => { + alert( + "문의사항이 성공적으로 전송되었습니다. 빠른 시일 내에 답변드리겠습니다.", + ); + setFormData({ + name: "", + email: "", + company: "", + subject: "", + message: "", + contactReason: "", + }); + setIsSubmitting(false); + }, 1000); + } catch (error) { + alert("문의사항 전송에 실패했습니다. 다시 시도해주세요."); + setIsSubmitting(false); + } + }; + + return ( +
+
+ {/* Header */} +
+
+

+ 문의하기 +

+

+ 궁금한 점이나 제안사항이 있으시면 언제든 연락주세요 +

+
+ {onBack && ( + + )} +
+ +
+ {/* Contact Form */} +
+ + + + ✉️ + 문의 보내기 + +

+ 아래 양식을 작성해주시면 빠르게 답변드리겠습니다. +

+
+ +
+ {/* Contact Reason */} +
+ +
+ {contactReasons.map((reason) => ( + + ))} +
+
+ +
+ {/* Name */} +
+ + +
+ + {/* Email */} +
+ + +
+
+ + {/* Company */} +
+ + +
+ + {/* Subject */} +
+ + +
+ + {/* Message */} +
+ +