diff --git a/package.json b/package.json index 12d342f81..6a207b91a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@sentry/react": "^8.22.0", "@tanstack/react-query": "^5.49.2", "@tanstack/react-query-devtools": "^5.49.2", + "@toss/use-funnel": "^1.4.2", "axios": "^1.7.2", "framer-motion": "^11.11.9", "mime": "^4.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a00f42ec2..f2892bd56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.49.2 version: 5.49.2(@tanstack/react-query@5.49.2(react@18.3.1))(react@18.3.1) + '@toss/use-funnel': + specifier: ^1.4.2 + version: 1.4.2(next@13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.2 version: 1.7.2 @@ -115,7 +118,7 @@ importers: version: 4.3.1(vite@5.3.2(@types/node@20.14.9)) '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.7.0(vite@5.3.2(@types/node@20.14.9)) + version: 3.7.0(@swc/helpers@0.5.2)(vite@5.3.2(@types/node@20.14.9)) chromatic: specifier: ^11.5.4 version: 11.5.4 @@ -1134,6 +1137,63 @@ packages: resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} engines: {node: '>=18'} + '@next/env@13.5.7': + resolution: {integrity: sha512-uVuRqoj28Ys/AI/5gVEgRAISd0KWI0HRjOO1CTpNgmX3ZsHb5mdn14Y59yk0IxizXdo7ZjsI2S7qbWnO+GNBcA==} + + '@next/swc-darwin-arm64@13.5.7': + resolution: {integrity: sha512-7SxmxMex45FvKtRoP18eftrDCMyL6WQVYJSEE/s7A1AW/fCkznxjEShKet2iVVzf89gWp8HbXGaL4hCaseux6g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@13.5.7': + resolution: {integrity: sha512-6iENvgyIkGFLFszBL4b1VfEogKC3TDPEB6/P/lgxmgXVXIV09Q4or1MVn+U/tYyYmm7oHMZ3oxGpHAyJ80nA6g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@13.5.7': + resolution: {integrity: sha512-P42jDX56wu9zEdVI+Xv4zyTeXB3DpqgE1Gb4bWrc0s2RIiDYr6uKBprnOs1hCGIwfVyByxyTw5Va66QCdFFNUg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@13.5.7': + resolution: {integrity: sha512-A06vkj+8X+tLRzSja5REm/nqVOCzR+x5Wkw325Q/BQRyRXWGCoNbQ6A+BR5M86TodigrRfI3lUZEKZKe3QJ9Bg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@13.5.7': + resolution: {integrity: sha512-UdHm7AlxIbdRdMsK32cH0EOX4OmzAZ4Xm+UVlS0YdvwLkI3pb7AoBEoVMG5H0Wj6Wpz6GNkrFguHTRLymTy6kw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@13.5.7': + resolution: {integrity: sha512-c50Y8xBKU16ZGj038H6C13iedRglxvdQHD/1BOtes56gwUrIRDX2Nkzn3mYtpz3Wzax0gfAF9C0Nqljt93IxvA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@13.5.7': + resolution: {integrity: sha512-NcUx8cmkA+JEp34WNYcKW6kW2c0JBhzJXIbw+9vKkt9m/zVJ+KfizlqmoKf04uZBtzFN6aqE2Fyv2MOd021WIA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@13.5.7': + resolution: {integrity: sha512-wXp+/3NVcuyJDED6gJiLXs5dqHaWO7moAB6aBtjlKZvsxBDxpcyjsfRbtHPeYtaT20zCkmPs69H0K25lrVZmlA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@13.5.7': + resolution: {integrity: sha512-PLyD3Dl6jTTkLG8AoqhPGd5pXtSs8wbqIhWPQt3yEMfnYld/dGYuF2YPs3YHaVFrijCIF9pXY3+QOyvP23Zn7g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1682,6 +1742,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.2': + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + '@swc/types@0.1.9': resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} @@ -1733,6 +1796,40 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@toss/assert@1.2.2': + resolution: {integrity: sha512-qK2G1LzI2ghY0aUOsz9mFiy2v/eNlMHG5qXdSogfGFLxHqFZ6KQWJQpnb9eN+dyHYIudBVWPZhbkljqnT8R3/g==} + + '@toss/react@1.8.1': + resolution: {integrity: sha512-jH3oo/7yctexuutj/YgQrddaK1bU2s5659dkJIXOe23bEjkY+lbhvEz2FLEhRjSo6k6ktPagpxO4AcdhCi5k5A==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18 + + '@toss/storage@1.4.1': + resolution: {integrity: sha512-jvnBXAQ/Fqqdt+gqYKeHYk7SzR2LX/FC50JIoVI0RldG1mDh1tebOqD7XkrZ89q77t/RwTuh60+8fjZDgend/g==} + + '@toss/use-funnel@1.4.2': + resolution: {integrity: sha512-qgfYhdoJh07D4+kyRgRE3Du5wEdecFOR9Ht3MjgFMYAJyGplNbD++hpD/FcXrGT5oa14KG0bB5bpp60KLmPkFw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + next: ^11.1 || ^12 || ^13 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + react-query: ^3 + + '@toss/use-query-param@1.3.1': + resolution: {integrity: sha512-GRA+6st46/88KgmP9PGx8mV9sxxkewwLMzBl25TpXPjnqHz+tiZdybVZMQ5UHj0Kzf7bZnHGF3kd5oko492vxA==} + peerDependencies: + next: ^11.1 || ^12 || ^13 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + + '@toss/utility-types@1.2.1': + resolution: {integrity: sha512-1y8s1bvmuhuMX/d6qR9mmvcgFZIKYIQqJbAIshlGArXkjk/ec67gXc5uByEV1Y7in9ZhrGNRmjD8DTH0988vpQ==} + + '@toss/utils@1.6.1': + resolution: {integrity: sha512-x6m8jLKWtAmCbxTLXbgTzJ5wZyRSUQPLpR/oLJP1ZK9ytXcRf03oA46W/+78kErUkEw/yQz2L+t2xFDHSeZ6IQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + '@trivago/prettier-plugin-sort-imports@4.3.0': resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} peerDependencies: @@ -2156,6 +2253,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2177,6 +2278,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + broadcast-channel@3.7.0: + resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==} + browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} @@ -2191,6 +2295,10 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2252,6 +2360,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -2264,6 +2375,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2385,6 +2499,10 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -2462,6 +2580,9 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2935,6 +3056,9 @@ packages: peerDependencies: glob: ^7.1.6 + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -3258,6 +3382,9 @@ packages: javascript-natural-sort@0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3358,6 +3485,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3422,6 +3552,9 @@ packages: peerDependencies: react: '>= 0.14.0' + match-sorter@6.3.4: + resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -3447,6 +3580,9 @@ packages: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + microseconds@0.2.0: + resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3538,6 +3674,9 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + nano-time@1.0.0: + resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3553,6 +3692,21 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next@13.5.7: + resolution: {integrity: sha512-W7KIRTE+hPcgGdq89P3mQLDX3m7pJ6nxSyC+YxYaUExE+cS4UledB+Ntk98tKoyhsv6fjb2TRAnD7VDvoqmeFg==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -3632,6 +3786,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + oblivious-set@1.0.0: + resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} + ohash@1.1.3: resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} @@ -3782,6 +3939,10 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.4.39: resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} engines: {node: ^10 || ^12 || >=14} @@ -3898,6 +4059,18 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-query@3.39.3: + resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -3970,6 +4143,9 @@ packages: rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4155,6 +4331,10 @@ packages: resolution: {integrity: sha512-8j30wDxQmkcqI0fWcSYFsUCjErsY1yTWbTW+yjbwM8DyW18Cud6CwbFRCxjFsH+2M0CjP6Pqs/m1PGI0vcQscQ==} hasBin: true + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -4215,6 +4395,19 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -4444,6 +4637,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unload@2.2.0: + resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -4531,6 +4727,10 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -5786,6 +5986,35 @@ snapshots: outvariant: 1.4.2 strict-event-emitter: 0.5.1 + '@next/env@13.5.7': {} + + '@next/swc-darwin-arm64@13.5.7': + optional: true + + '@next/swc-darwin-x64@13.5.7': + optional: true + + '@next/swc-linux-arm64-gnu@13.5.7': + optional: true + + '@next/swc-linux-arm64-musl@13.5.7': + optional: true + + '@next/swc-linux-x64-gnu@13.5.7': + optional: true + + '@next/swc-linux-x64-musl@13.5.7': + optional: true + + '@next/swc-win32-arm64-msvc@13.5.7': + optional: true + + '@next/swc-win32-ia32-msvc@13.5.7': + optional: true + + '@next/swc-win32-x64-msvc@13.5.7': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6407,7 +6636,7 @@ snapshots: '@swc/core-win32-x64-msvc@1.6.6': optional: true - '@swc/core@1.6.6': + '@swc/core@1.6.6(@swc/helpers@0.5.2)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.9 @@ -6422,9 +6651,14 @@ snapshots: '@swc/core-win32-arm64-msvc': 1.6.6 '@swc/core-win32-ia32-msvc': 1.6.6 '@swc/core-win32-x64-msvc': 1.6.6 + '@swc/helpers': 0.5.2 '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.2': + dependencies: + tslib: 2.6.3 + '@swc/types@0.1.9': dependencies: '@swc/counter': 0.1.3 @@ -6470,6 +6704,51 @@ snapshots: dependencies: '@testing-library/dom': 10.1.0 + '@toss/assert@1.2.2': + dependencies: + '@toss/utils': 1.6.1 + + '@toss/react@1.8.1(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.7 + '@toss/storage': 1.4.1 + '@toss/utils': 1.6.1 + classnames: 2.5.1 + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + react: 18.3.1 + + '@toss/storage@1.4.1': + dependencies: + '@babel/runtime': 7.24.7 + + '@toss/use-funnel@1.4.2(next@13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@toss/assert': 1.2.2 + '@toss/react': 1.8.1(react@18.3.1) + '@toss/storage': 1.4.1 + '@toss/use-query-param': 1.3.1(next@13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@toss/utils': 1.6.1 + fast-deep-equal: 3.1.3 + next: 13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-query: 3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@toss/use-query-param@1.3.1(next@13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + next: 13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@toss/utility-types@1.2.1': {} + + '@toss/utils@1.6.1': + dependencies: + '@babel/runtime': 7.24.7 + '@toss/utility-types': 1.2.1 + date-fns: 2.30.0 + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.3.2)': dependencies: '@babel/generator': 7.17.7 @@ -6749,9 +7028,9 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.7.0(vite@5.3.2(@types/node@20.14.9))': + '@vitejs/plugin-react-swc@3.7.0(@swc/helpers@0.5.2)(vite@5.3.2(@types/node@20.14.9))': dependencies: - '@swc/core': 1.6.6 + '@swc/core': 1.6.6(@swc/helpers@0.5.2) vite: 5.3.2(@types/node@20.14.9) transitivePeerDependencies: - '@swc/helpers' @@ -7001,6 +7280,8 @@ snapshots: base64-js@1.5.1: {} + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} bl@4.1.0: @@ -7039,6 +7320,17 @@ snapshots: dependencies: fill-range: 7.1.1 + broadcast-channel@3.7.0: + dependencies: + '@babel/runtime': 7.24.7 + detect-node: 2.1.0 + js-sha3: 0.8.0 + microseconds: 0.2.0 + nano-time: 1.0.0 + oblivious-set: 1.0.0 + rimraf: 3.0.2 + unload: 2.2.0 + browser-assert@1.2.1: {} browserslist@4.23.1: @@ -7055,6 +7347,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} call-bind@1.0.7: @@ -7121,6 +7417,8 @@ snapshots: dependencies: consola: 3.2.3 + classnames@2.5.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -7129,6 +7427,8 @@ snapshots: cli-width@4.1.0: {} + client-only@0.0.1: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -7250,6 +7550,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.24.7 + date-fns@3.6.0: {} debug@2.6.9: @@ -7321,6 +7625,8 @@ snapshots: detect-indent@6.1.0: {} + detect-node@2.1.0: {} + diff-sequences@29.6.3: {} dir-glob@3.0.1: @@ -7993,6 +8299,8 @@ snapshots: '@types/glob': 7.2.0 glob: 7.2.3 + glob-to-regexp@0.4.1: {} + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -8290,6 +8598,8 @@ snapshots: javascript-natural-sort@0.7.1: {} + js-sha3@0.8.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -8394,6 +8704,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.throttle@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -8456,6 +8768,11 @@ snapshots: dependencies: react: 18.3.1 + match-sorter@6.3.4: + dependencies: + '@babel/runtime': 7.24.7 + remove-accents: 0.5.0 + media-typer@0.3.0: {} memoizerific@1.11.3: @@ -8475,6 +8792,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + microseconds@0.2.0: {} + mime-db@1.52.0: {} mime-types@2.1.35: @@ -8557,6 +8876,10 @@ snapshots: mute-stream@1.0.0: {} + nano-time@1.0.0: + dependencies: + big-integer: 1.6.52 + nanoid@3.3.7: {} natural-compare@1.4.0: {} @@ -8565,6 +8888,31 @@ snapshots: neo-async@2.6.2: {} + next@13.5.7(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 13.5.7 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001639 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.24.7)(react@18.3.1) + watchpack: 2.4.0 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.7 + '@next/swc-darwin-x64': 13.5.7 + '@next/swc-linux-arm64-gnu': 13.5.7 + '@next/swc-linux-arm64-musl': 13.5.7 + '@next/swc-linux-x64-gnu': 13.5.7 + '@next/swc-linux-x64-musl': 13.5.7 + '@next/swc-win32-arm64-msvc': 13.5.7 + '@next/swc-win32-ia32-msvc': 13.5.7 + '@next/swc-win32-x64-msvc': 13.5.7 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -8650,6 +8998,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + oblivious-set@1.0.0: {} + ohash@1.1.3: {} on-finished@2.4.1: @@ -8789,6 +9139,12 @@ snapshots: possible-typed-array-names@1.0.0: {} + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + postcss@8.4.39: dependencies: nanoid: 3.3.7 @@ -8909,6 +9265,15 @@ snapshots: react-is@18.3.1: {} + react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.7 + broadcast-channel: 3.7.0 + match-sorter: 6.3.4 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-refresh@0.14.2: {} react-router-dom@6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -9009,6 +9374,8 @@ snapshots: hast-util-to-string: 3.0.0 unist-util-visit: 5.0.0 + remove-accents@0.5.0: {} + require-directory@2.1.1: {} requireindex@1.2.0: {} @@ -9246,6 +9613,8 @@ snapshots: - supports-color - utf-8-validate + streamsearch@1.1.0: {} + strict-event-emitter@0.5.1: {} string-width@4.2.3: @@ -9321,6 +9690,13 @@ snapshots: strip-json-comments@3.1.1: {} + styled-jsx@5.1.1(@babel/core@7.24.7)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.24.7 + stylis@4.2.0: {} supports-color@5.5.0: @@ -9535,6 +9911,11 @@ snapshots: universalify@2.0.1: {} + unload@2.2.0: + dependencies: + '@babel/runtime': 7.24.7 + detect-node: 2.1.0 + unpipe@1.0.0: {} unplugin@1.0.1: @@ -9615,6 +9996,11 @@ snapshots: walk-up-path@3.0.1: {} + watchpack@2.4.0: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 diff --git a/src/App.tsx b/src/App.tsx index 74fdba759..cedc6fcdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,12 +10,13 @@ import ErrorBoundary from '@/common/component/ErrorBoundary/ErrorBoundary'; import { HTTPError } from '@/shared/api/HTTPError'; import Header from '@/shared/component/Header/Header'; import Login from '@/shared/component/Login/Login'; -import ModalContainer from '@/shared/component/Modal/ModalContainer'; import SNB from '@/shared/component/SideNavBar/LeftSidebar'; import { HTTP_STATUS_CODE } from '@/shared/constant/api'; import { PATH } from '@/shared/constant/path'; import ErrorPage from '@/shared/page/errorPage/ErrorPage'; +import ModalFunnel from './shared/component/Modal/ModalFunnel'; + const App = () => { const navigate = useNavigate(); @@ -48,7 +49,7 @@ const App = () => { return ( - +
diff --git a/src/common/asset/svg/ic_activity_tag.svg b/src/common/asset/svg/ic_activity_tag.svg new file mode 100644 index 000000000..0bf5a532b --- /dev/null +++ b/src/common/asset/svg/ic_activity_tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_block_create.svg b/src/common/asset/svg/ic_block_create.svg new file mode 100644 index 000000000..5f7da8779 --- /dev/null +++ b/src/common/asset/svg/ic_block_create.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/common/asset/svg/ic_calendar_tag.svg b/src/common/asset/svg/ic_calendar_tag.svg new file mode 100644 index 000000000..b2406f936 --- /dev/null +++ b/src/common/asset/svg/ic_calendar_tag.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/common/asset/svg/ic_event_circle.svg b/src/common/asset/svg/ic_event_circle.svg deleted file mode 100644 index e3ee1d32e..000000000 --- a/src/common/asset/svg/ic_event_circle.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/common/asset/svg/ic_event_gray.svg b/src/common/asset/svg/ic_event_gray.svg new file mode 100644 index 000000000..3dc8be5ec --- /dev/null +++ b/src/common/asset/svg/ic_event_gray.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/common/asset/svg/ic_file_delete.svg b/src/common/asset/svg/ic_file_delete.svg index 555a3e434..e7f78b971 100644 --- a/src/common/asset/svg/ic_file_delete.svg +++ b/src/common/asset/svg/ic_file_delete.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/common/asset/svg/ic_file_preview.svg b/src/common/asset/svg/ic_file_preview.svg new file mode 100644 index 000000000..287805475 --- /dev/null +++ b/src/common/asset/svg/ic_file_preview.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_invite.svg b/src/common/asset/svg/ic_invite.svg new file mode 100644 index 000000000..a34cfa7b8 --- /dev/null +++ b/src/common/asset/svg/ic_invite.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_invite_delete.svg b/src/common/asset/svg/ic_invite_delete.svg new file mode 100644 index 000000000..a6c49cb06 --- /dev/null +++ b/src/common/asset/svg/ic_invite_delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_invite_profile.svg b/src/common/asset/svg/ic_invite_profile.svg new file mode 100644 index 000000000..b9b54f8c8 --- /dev/null +++ b/src/common/asset/svg/ic_invite_profile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/common/asset/svg/ic_meeting_circle.svg b/src/common/asset/svg/ic_meeting_circle.svg deleted file mode 100644 index e8c0d897c..000000000 --- a/src/common/asset/svg/ic_meeting_circle.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/common/asset/svg/ic_meeting_gray.svg b/src/common/asset/svg/ic_meeting_gray.svg new file mode 100644 index 000000000..d80accfef --- /dev/null +++ b/src/common/asset/svg/ic_meeting_gray.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/common/asset/svg/ic_member_tag.svg b/src/common/asset/svg/ic_member_tag.svg new file mode 100644 index 000000000..0bf5a532b --- /dev/null +++ b/src/common/asset/svg/ic_member_tag.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_notice_circle.svg b/src/common/asset/svg/ic_notice_circle.svg deleted file mode 100644 index f95c22b59..000000000 --- a/src/common/asset/svg/ic_notice_circle.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/common/asset/svg/ic_notice_gray.svg b/src/common/asset/svg/ic_notice_gray.svg new file mode 100644 index 000000000..f17811bf5 --- /dev/null +++ b/src/common/asset/svg/ic_notice_gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_recruiting_gray.svg b/src/common/asset/svg/ic_recruiting_gray.svg new file mode 100644 index 000000000..f583fb0b2 --- /dev/null +++ b/src/common/asset/svg/ic_recruiting_gray.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/common/asset/svg/ic_study_circle.svg b/src/common/asset/svg/ic_study_circle.svg deleted file mode 100644 index 41761ee06..000000000 --- a/src/common/asset/svg/ic_study_circle.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/common/asset/svg/ic_study_gray.svg b/src/common/asset/svg/ic_study_gray.svg new file mode 100644 index 000000000..337a8edbe --- /dev/null +++ b/src/common/asset/svg/ic_study_gray.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/common/asset/svg/ic_team_profile_add.svg b/src/common/asset/svg/ic_team_profile_add.svg index 5190041d4..79159526d 100644 --- a/src/common/asset/svg/ic_team_profile_add.svg +++ b/src/common/asset/svg/ic_team_profile_add.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/common/asset/svg/ic_team_profile_delete.svg b/src/common/asset/svg/ic_team_profile_delete.svg index fe12e3904..b3761b592 100644 --- a/src/common/asset/svg/ic_team_profile_delete.svg +++ b/src/common/asset/svg/ic_team_profile_delete.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/src/common/asset/svg/ic_upload_file.svg b/src/common/asset/svg/ic_upload_file.svg new file mode 100644 index 000000000..ca0a9f21a --- /dev/null +++ b/src/common/asset/svg/ic_upload_file.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_warning.svg b/src/common/asset/svg/ic_warning.svg new file mode 100644 index 000000000..8d6c0b2a2 --- /dev/null +++ b/src/common/asset/svg/ic_warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/asset/svg/ic_workspace_success.svg b/src/common/asset/svg/ic_workspace_success.svg new file mode 100644 index 000000000..72b1fd271 --- /dev/null +++ b/src/common/asset/svg/ic_workspace_success.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/component/CommandButton/CommandButton.style.ts b/src/common/component/CommandButton/CommandButton.style.ts index c71714495..11a6b0f5f 100644 --- a/src/common/component/CommandButton/CommandButton.style.ts +++ b/src/common/component/CommandButton/CommandButton.style.ts @@ -72,7 +72,5 @@ export const childrenStyle = css({ display: 'flex', alignItems: 'center', - padding: '0.4rem', - gap: '0.4rem', }); diff --git a/src/common/component/DatePicker/index.tsx b/src/common/component/DatePicker/index.tsx index beb745296..759340fa7 100644 --- a/src/common/component/DatePicker/index.tsx +++ b/src/common/component/DatePicker/index.tsx @@ -8,15 +8,32 @@ import { useOverlay } from '@/common/hook/useOverlay'; interface DatePickerProps { variant: 'single' | 'range'; triggerWidth?: string; + onChange: (selectedDate: Date | null, endDate: Date | null) => void; + defaultSelectedDate?: Date; + defaultEndDate?: Date; } -const DatePicker = ({ variant, triggerWidth = '10.3rem' }: DatePickerProps) => { +const DatePicker = ({ + variant, + triggerWidth = '10.3rem', + onChange, + defaultSelectedDate, + defaultEndDate, +}: DatePickerProps) => { const { isOpen, close, toggle } = useOverlay(); const ref = useOutsideClick(close); - const { selectedDate, endDate, handleSelectDate, clearDates } = useDatePicker(variant); + + const initialSelectedDate = defaultSelectedDate ? new Date(defaultSelectedDate) : null; + const initialEndDate = defaultEndDate ? new Date(defaultEndDate) : null; + + const { selectedDate, endDate, handleSelectDate, clearDates } = useDatePicker( + variant, + onChange, + initialSelectedDate, + initialEndDate + ); const handleInputClick = () => { - // 캘린더가 닫혀 있고, 시작날짜와 종료날짜가 모두 선택된 경우에만 날짜 초기화 if (!isOpen && selectedDate && endDate) { clearDates(); } @@ -26,16 +43,16 @@ const DatePicker = ({ variant, triggerWidth = '10.3rem' }: DatePickerProps) => { return (
{isOpen && ( diff --git a/src/common/component/Modal/Modal.style.ts b/src/common/component/Modal/Wrapper/ModalWrapper.style.ts similarity index 78% rename from src/common/component/Modal/Modal.style.ts rename to src/common/component/Modal/Wrapper/ModalWrapper.style.ts index 136a84cb1..02ee6436e 100644 --- a/src/common/component/Modal/Modal.style.ts +++ b/src/common/component/Modal/Wrapper/ModalWrapper.style.ts @@ -7,29 +7,37 @@ export const backgroundStyle = css({ display: 'flex', justifyContent: 'center', alignItems: 'center', + position: 'fixed', top: 0, left: 0, zIndex: theme.zIndex.overlayHigh, + width: '100vw', height: '100vh', + backgroundColor: 'rgba(0, 0, 0, 0.5)', animation: `${fadeIn} 0.2s ease-in`, }); export const dialogStyle = css({ - display: 'block', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + position: 'fixed', top: '50%', left: '50%', - width: '51.1rem', - zIndex: theme.zIndex.overlayTop, - paddingTop: '4.8rem', - paddingBottom: '4.8rem', - borderRadius: '16px', + + width: '37.6rem', + height: '55.4rem', + + padding: '2rem 2rem 3.2rem 2rem', + borderRadius: '8px', border: 'none', outline: 'none', + background: theme.colors.white, transform: 'translate(-50%, -50%)', }); diff --git a/src/common/component/Modal/Modal.tsx b/src/common/component/Modal/Wrapper/ModalWrapper.tsx similarity index 90% rename from src/common/component/Modal/Modal.tsx rename to src/common/component/Modal/Wrapper/ModalWrapper.tsx index 9406b8064..e61ed1845 100644 --- a/src/common/component/Modal/Modal.tsx +++ b/src/common/component/Modal/Wrapper/ModalWrapper.tsx @@ -4,7 +4,7 @@ import { ReactNode, useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import { backgroundStyle, dialogStyle } from '@/common/component/Modal/Modal.style'; +import { backgroundStyle, dialogStyle } from '@/common/component/Modal/Wrapper/ModalWrapper.style'; interface ModalProps { isOpen: boolean; @@ -12,7 +12,7 @@ interface ModalProps { onClose?: () => void; } -const Modal = ({ isOpen, children, onClose }: ModalProps) => { +const ModalWrapper = ({ isOpen, children, onClose }: ModalProps) => { const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -52,4 +52,4 @@ const Modal = ({ isOpen, children, onClose }: ModalProps) => { ); }; -export default Modal; +export default ModalWrapper; diff --git a/src/common/component/Modal/index.tsx b/src/common/component/Modal/index.tsx new file mode 100644 index 000000000..d7125b0a9 --- /dev/null +++ b/src/common/component/Modal/index.tsx @@ -0,0 +1,11 @@ +import ModalWrapper from '@/common/component/Modal/Wrapper/ModalWrapper'; + +import ModalBody from '@/shared/component/Modal/Body/ModalBody'; +import ModalFooter from '@/shared/component/Modal/Footer/ModalFooter'; +import ModalHeader from '@/shared/component/Modal/Header/ModalHeader'; + +export const Modal = Object.assign(ModalWrapper, { + Header: ModalHeader, + Body: ModalBody, + Footer: ModalFooter, +}); diff --git a/src/common/hook/useDatePicker.ts b/src/common/hook/useDatePicker.ts index a9c4c4ffc..c9a999357 100644 --- a/src/common/hook/useDatePicker.ts +++ b/src/common/hook/useDatePicker.ts @@ -2,31 +2,41 @@ import { isBefore } from 'date-fns'; import { useState } from 'react'; -export const useDatePicker = (variant: 'single' | 'range') => { - const [selectedDate, setSelectedDate] = useState(null); - const [endDate, setEndDate] = useState(null); +export const useDatePicker = ( + variant: 'single' | 'range', + onChange: (selectedDate: Date | null, endDate: Date | null) => void, + initialSelectedDate: Date | null = null, + initialEndDate: Date | null = null +) => { + const [selectedDate, setSelectedDate] = useState(initialSelectedDate); + const [endDate, setEndDate] = useState(initialEndDate); const handleSelectDate = (date: Date) => { if (variant === 'range') { if (!selectedDate || (selectedDate && endDate)) { setSelectedDate(date); setEndDate(null); + onChange(date, null); } else { if (isBefore(date, selectedDate)) { setEndDate(selectedDate); setSelectedDate(date); + onChange(date, selectedDate); } else { setEndDate(date); + onChange(selectedDate, date); } } } else { setSelectedDate(date); + onChange(date, null); } }; const clearDates = () => { setSelectedDate(null); setEndDate(null); + onChange(null, null); }; return { diff --git a/src/common/style/scroll.ts b/src/common/style/scroll.ts index 72658c9b3..8dd3d5104 100644 --- a/src/common/style/scroll.ts +++ b/src/common/style/scroll.ts @@ -2,12 +2,15 @@ import { css } from '@emotion/react'; import { theme } from '@/common/style/theme/theme'; -export const scrollStyle = css` - ::-webkit-scrollbar { - width: 0.8rem; - } - ::-webkit-scrollbar-thumb { - background: ${theme.colors.gray_300}; - border-radius: 10rem; - } -`; +export const scrollStyle = { + '::-webkit-scrollbar': { + width: '0.8rem', + }, + '::-webkit-scrollbar-thumb': { + background: theme.colors.gray_300, + borderRadius: '10rem', + }, + '::-webkit-scrollbar-track': { + background: 'transparent', + }, +}; diff --git a/src/page/archiving/index/component/DocumentBar/Item/Item.tsx b/src/page/archiving/index/component/DocumentBar/Item/Item.tsx index 32eb18346..05cfbdc3a 100644 --- a/src/page/archiving/index/component/DocumentBar/Item/Item.tsx +++ b/src/page/archiving/index/component/DocumentBar/Item/Item.tsx @@ -1,6 +1,5 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { ReactNode } from 'react'; -import { useLocation } from 'react-router-dom'; import TrashBox from '@/common/asset/svg/ic_delete.svg?react'; import Download from '@/common/asset/svg/ic_download.svg?react'; @@ -10,8 +9,6 @@ import Text from '@/common/component/Text/Text'; import { containerStyle, fileNameStyle } from '@/page/archiving/index/component/DocumentBar/Item/Item.style'; import { downloadDocument } from '@/page/archiving/index/util/document'; -import { useOpenModal } from '@/shared/store/modal'; - interface ItemProps { documentId: number; children?: ReactNode; @@ -21,13 +18,7 @@ interface ItemProps { fileName: string; } -const Item = ({ documentId, children, fileUrl, fileName }: ItemProps) => { - const location = useLocation(); - const searchParams = new URLSearchParams(location.search); - const teamId = searchParams.get('teamId'); - - const openModal = useOpenModal(); - +const Item = ({ children, fileUrl, fileName }: ItemProps) => { const onClickDocumentItem = () => { window.open(fileUrl); }; @@ -39,8 +30,6 @@ const Item = ({ documentId, children, fileUrl, fileName }: ItemProps) => { const handleTrashBoxClick = (e: React.MouseEvent) => { e.stopPropagation(); - - openModal('delete', { teamId: +teamId!, itemId: documentId, itemType: 'docs' }); }; return ( diff --git a/src/page/archiving/index/component/DocumentBar/Selected/Selected.tsx b/src/page/archiving/index/component/DocumentBar/Selected/Selected.tsx index b7f1c433f..c63e76d9d 100644 --- a/src/page/archiving/index/component/DocumentBar/Selected/Selected.tsx +++ b/src/page/archiving/index/component/DocumentBar/Selected/Selected.tsx @@ -14,8 +14,6 @@ import { Block } from '@/page/archiving/index/type/blockType'; import { DocumentType } from '@/page/archiving/index/type/documentType'; import { formattingDate } from '@/page/archiving/index/util/date'; -import { useOpenModal } from '@/shared/store/modal'; - interface SelectedProps { selectedBlock: Block; onClose: () => void; @@ -32,12 +30,6 @@ const Selected = ({ selectedBlock }: SelectedProps) => { const startDate = formattingDate(selectedBlock.startDate); const endDate = formattingDate(selectedBlock.endDate); - const openModal = useOpenModal(); - - const handleDeleteClick = () => { - openModal('delete', { teamId: +teamId!, itemId: selectedBlock.timeBlockId, itemType: 'block' }); - }; - return ( {BLOCK_ICON.find((icon) => icon.name === selectedBlock.blockType)?.icon?.(selectedBlock.color)} @@ -45,7 +37,7 @@ const Selected = ({ selectedBlock }: SelectedProps) => { {selectedBlock.name} - diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style.ts index 916c8f21e..37e098126 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style.ts +++ b/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style.ts @@ -18,6 +18,5 @@ export const buttonStyle = (isActive: boolean) => }); export const textStyle = css({ - color: theme.colors.gray_700, - fontWeight: 400, + color: theme.colors.gray_500, }); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.tsx index da1867160..9149f8b1e 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.tsx @@ -1,31 +1,24 @@ import { useState } from 'react'; -import Button from '@/common/component/Button/Button'; +import DatePicker from '@/common/component/DatePicker'; import Flex from '@/common/component/Flex/Flex'; import Input from '@/common/component/Input/Input'; +import { Modal } from '@/common/component/Modal'; import Text from '@/common/component/Text/Text'; -import { - buttonStyle, - textStyle, -} from '@/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style'; -import BlockDate from '@/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate'; +import { textStyle } from '@/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style'; import BlockIcon from '@/page/archiving/index/component/TimeBlockModal/component/Block/Icon/BlockIcon'; import BlockBox from '@/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox'; import { BLOCK_ICON } from '@/page/archiving/index/component/TimeBlockModal/constant/iconBlock'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; import { useBlockContext } from '@/shared/hook/common/useBlockContext'; -interface BlockModalProps { - isVisible: boolean; -} +const BlockModal = () => { + const [selectedIcon, setSelectedIcon] = useState(-1); -const BlockModal = ({ isVisible }: BlockModalProps) => { - const [selectedIcon, setSelectedIcon] = useState(-1); - const [isDateRangeValid, setIsDateRangeValid] = useState(false); - - const { formData, setFormData, nextStep } = useBlockContext(); + const { formData, setFormData } = useBlockContext(); + const { nextStep } = useFunnel(); const handleBlockNameChange = (e: React.ChangeEvent) => { if (e.target.value.length <= 25) { @@ -33,12 +26,7 @@ const BlockModal = ({ isVisible }: BlockModalProps) => { } }; - const isButtonActive = - formData.blockName.trim() !== '' && - selectedIcon !== -1 && - formData.startDate.length === 10 && - formData.endDate.length === 10 && - isDateRangeValid; + const isButtonActive = formData.blockName.trim() !== '' && selectedIcon !== -1; const handleNext = () => { if (isButtonActive) { @@ -52,66 +40,47 @@ const BlockModal = ({ isVisible }: BlockModalProps) => { } }; - if (!isVisible) return null; + const handleDateChange = () => { + // 날짜 선택 코드 추후 작성 필요 + }; return ( - - - - - - + <> + + + + + + - - - - - {formData.blockName.length} / 25 - - - + + + + + {formData.blockName.length} / 25 + + + - - - setFormData({ startDate: date })} - onSetEndDate={(date) => setFormData({ endDate: date })} - onSetIsDateRangeValid={setIsDateRangeValid} - /> - - - - - + + + + + + + ); }; diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.style.ts deleted file mode 100644 index c837bdec2..000000000 --- a/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.style.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { css } from '@emotion/react'; - -import { theme } from '@/common/style/theme/theme'; - -export const textStyle = css({ - color: theme.colors.gray_500, - fontSize: '2rem', -}); - -export const supportStyle = css({ - marginTop: '0.8rem', - ...theme.text.body07, - fontWeight: 400, -}); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.tsx deleted file mode 100644 index 2c6b594e4..000000000 --- a/src/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { css } from '@emotion/react'; - -import Flex from '@/common/component/Flex/Flex'; -import Input from '@/common/component/Input/Input'; -import SupportingText from '@/common/component/SupportingText/SupportingText'; - -import { - supportStyle, - textStyle, -} from '@/page/archiving/index/component/TimeBlockModal/component/Block/Date/BlockDate.style'; -import useDateRange from '@/page/archiving/index/component/TimeBlockModal/hook/common/useDateRange'; - -interface BlockDateProps { - startDate: string; - endDate: string; - onSetStartDate: (date: string) => void; - onSetEndDate: (date: string) => void; - onSetIsDateRangeValid: (isValid: boolean) => void; -} - -const BlockDate = ({ startDate, endDate, onSetStartDate, onSetEndDate, onSetIsDateRangeValid }: BlockDateProps) => { - const { dates, validation, handleChange } = useDateRange( - startDate, - endDate, - (date: string) => onSetStartDate(date), - (date: string) => onSetEndDate(date), - onSetIsDateRangeValid - ); - - const inputStyle = (value: string) => css` - text-align: ${value.length === 10 ? 'center' : 'left'}; - `; - - return ( - <> - - handleChange('startDate', e.target.value, validation.isEndDateValid, true)} - maxLength={10} - isError={validation.isStartDateError} - /> -

~

- handleChange('endDate', e.target.value, validation.isStartDateValid, false)} - maxLength={10} - isError={validation.isEndDateError} - /> -
- -
- {(validation.isStartDateError || validation.isEndDateError) && ( - - {validation.errorMessage} - - )} -
- - ); -}; - -export default BlockDate; diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Block/Icon/BlockIcon.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Block/Icon/BlockIcon.style.ts index c46b957b5..a5d2329c3 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Block/Icon/BlockIcon.style.ts +++ b/src/page/archiving/index/component/TimeBlockModal/component/Block/Icon/BlockIcon.style.ts @@ -7,15 +7,16 @@ export const iconStyle = css({ justifyContent: 'center', alignItems: 'center', - width: '5rem', - height: '5rem', + width: '4rem', + height: '4rem', borderRadius: '100%', - border: `1.2px solid ${theme.colors.gray_400}`, overflow: 'hidden', cursor: 'pointer', + backgroundColor: theme.colors.gray_100, + '&:hover': { backgroundColor: theme.colors.blue_100, }, @@ -32,5 +33,7 @@ export const boxStyle = css({ justifyContent: 'center', flexDirection: 'row', + width: '100%', + gap: '1.2rem', }); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.style.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.tsx index 34e649bc5..89824c64a 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/component/Box/BlockBox.tsx @@ -10,7 +10,7 @@ interface BlockBoxProps { const BlockBox = ({ title, children }: BlockBoxProps) => { return ( - + {title} diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.style.ts index df9f06239..2780e2b5e 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.style.ts +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.style.ts @@ -9,17 +9,28 @@ export const boxStyle = css({ color: theme.colors.gray_500, }); -export const textStyle = css({ - color: theme.colors.gray_500, +export const colorStyle = css({ + color: theme.colors.gray_800, }); -export const buttonStyle = css({ - padding: '0', - width: '6.4rem', +export const text1Style = css([ + colorStyle, + { + fontWeight: 500, + marginTop: '1.2rem', + }, +]); - textDecoration: 'underline', - ...theme.text.body06, - fontWeight: 600, +export const text2Style = css([ + colorStyle, + { + marginTop: '1.6rem', + }, +]); - color: theme.colors.gray_500, -}); +export const text3Style = css([ + colorStyle, + { + marginTop: '0.5rem', + }, +]); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.tsx index fee47f946..3581cd78c 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.tsx @@ -1,12 +1,15 @@ import { Dispatch, SetStateAction } from 'react'; +import UploadIcon from '@/common/asset/svg/ic_upload_file.svg?react'; import Button from '@/common/component/Button/Button'; import Flex from '@/common/component/Flex/Flex'; import Text from '@/common/component/Text/Text'; import { boxStyle, - buttonStyle, + text1Style, + text2Style, + text3Style, } from '@/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd.style'; import useFile from '@/page/archiving/index/component/TimeBlockModal/hook/common/useFile'; @@ -33,27 +36,36 @@ const BlockAdd = ({ files, onFilesChange, setFileUrls, setUploadStatus }: BlockA direction: 'column', justify: 'center', align: 'center', - padding: '3.2rem 6.35rem', + padding: '3.2rem 0rem', width: '100%', }} css={boxStyle} onDragOver={handleDragOver} onDrop={(event) => handleDrop(event)}> - - - 업로드할 파일을 여기로 드래그 하세요 - - 또는 - - 하여 - - - 업로드할 파일을 선택하세요 + + + + + 업로드할 파일을 끌어다 놓으세요. + + JPEG, PNG, PDF, Word 형식의 파일을 업로드할 수 있습니다. + + + 최대 파일 크기는 50MB입니다. + + ); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style.ts index 0991959a7..9570d6196 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style.ts +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style.ts @@ -2,16 +2,6 @@ import { css } from '@emotion/react'; import { theme } from '@/common/style/theme/theme'; -export const borderStyle = css({ - borderRadius: '8px', - border: `1px solid ${theme.colors.gray_300}`, - - width: '37.5rem', -}); - export const textStyle = css({ color: theme.colors.gray_800, - fontWeight: 500, - - maxWidth: '30rem', }); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.tsx index bcd6c0d38..52b0db6c8 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.tsx @@ -1,32 +1,46 @@ import Delete from '@/common/asset/svg/ic_file_delete.svg?react'; +import File from '@/common/asset/svg/ic_file_preview.svg?react'; import Flex from '@/common/component/Flex/Flex'; -import Spinner from '@/common/component/Spinner/Spinner'; import Text from '@/common/component/Text/Text'; -import { - borderStyle, - textStyle, -} from '@/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style'; +import { textStyle } from '@/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal.style'; +import '@/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem.style'; interface BlockItemProps { title: string; + fileSize: string; + uploadedSize: string; onDelete: () => void; isUploading: boolean; } -const BlockItem = ({ title, onDelete, isUploading }: BlockItemProps) => { +const BlockItem = ({ title, fileSize, uploadedSize, onDelete }: BlockItemProps) => { + /* 추가해야 할 것 : 프로그래스바 ==> 서버로직 짤때 컴포넌트로 따로 빼서 적용할 것!*/ + return ( - - {title} - - {isUploading ? ( - - ) : ( - - )} + styles={{ + direction: 'row', + align: 'center', + justify: 'space-between', + padding: '0.7rem 0rem', + width: '100%', + }}> + + + + {title} + + {fileSize} 중 {uploadedSize} + + + + + ); }; diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.style.ts b/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.style.ts index 96e3fafc3..1336e63a7 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.style.ts +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.style.ts @@ -1,41 +1,28 @@ import { css } from '@emotion/react'; -import { theme } from '@/common/style/theme/theme'; +import { scrollStyle } from '@/common/style/scroll'; -export const scrollStyle = css({ +export const scrollContainerStyle = css({ display: 'flex', flexDirection: 'column', - maxHeight: '18rem', - width: '38.5rem', + maxHeight: '12rem', + width: '100%', - gap: '0.8rem', + gap: '1.2rem', position: 'relative', - paddingRight: '1rem', overflowY: 'auto', + overflowX: 'hidden', boxSizing: 'content-box', - '&::-webkit-scrollbar': { - width: '1rem', - }, - - '&::-webkit-scrollbar-thumb': { - backgroundColor: theme.colors.gray_300, - borderRadius: '4px', - }, - - '&::-webkit-scrollbar-track': { - backgroundColor: 'transparent', - }, + ...scrollStyle, }); export const flexStyle = css({ flexDirection: 'column', justifyContent: 'space-between', alignItems: 'center', - height: '55.11rem', - paddingLeft: '6.8rem', - paddingRight: '6.8rem', - gap: '2.4rem', + width: '100%', + gap: '2rem', }); diff --git a/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.tsx b/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.tsx index 98c50f9ae..9e5cf477a 100644 --- a/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.tsx @@ -1,34 +1,29 @@ import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; -import Button from '@/common/component/Button/Button'; import Flex from '@/common/component/Flex/Flex'; +import { Modal } from '@/common/component/Modal'; import BlockAdd from '@/page/archiving/index/component/TimeBlockModal/component/Upload/File/Add/BlockAdd'; import BlockItem from '@/page/archiving/index/component/TimeBlockModal/component/Upload/File/List/BlockItem'; import { flexStyle, - scrollStyle, + scrollContainerStyle, } from '@/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal.style'; import { useDeleteFileMutation } from '@/page/archiving/index/component/TimeBlockModal/hook/api/useDeleteFileMutation'; import { usePostTimeBlockMutation } from '@/page/archiving/index/component/TimeBlockModal/hook/api/usePostTimeBlockMutation'; import { formatDatePost } from '@/page/archiving/index/component/TimeBlockModal/util/date'; import { Files } from '@/shared/api/time-blocks/team/time-block/type'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; import { useBlockContext } from '@/shared/hook/common/useBlockContext'; import { useCloseModal } from '@/shared/store/modal'; import { useToastAction } from '@/shared/store/toast'; -interface UploadModalProps { - isVisible: boolean; -} - -const UploadModal = ({ isVisible }: UploadModalProps) => { +const UploadModal = () => { const location = useLocation(); const searchParams = new URLSearchParams(location.search); const teamId = searchParams.get('teamId'); - const { formData, reset } = useBlockContext(); + const { formData, resetFormData } = useBlockContext(); const closeModal = useCloseModal(); const [files, setFiles] = useState([]); @@ -83,8 +78,6 @@ const UploadModal = ({ isVisible }: UploadModalProps) => { } }; - if (!isVisible) return null; - const data = { name: formData.blockName, color: formData.blockColor, @@ -99,43 +92,48 @@ const UploadModal = ({ isVisible }: UploadModalProps) => { onSuccess: () => { createToast('활동 블록이 생성되었습니다', 'success'); closeModal(); - reset(); + resetFormData(); }, }); }; return ( - - - - -
- {files.map((file) => ( - handleDelete(file.name)} - isUploading={!uploadStatus[file.name]} + <> + + + + + - ))} -
-
- -
+
+ {files.map((file) => ( + handleDelete(file.name)} + /* 임의의 값 넣었음! 추후 서버 로직 다시 짤때 바꿀것!!*/ + fileSize="2.4MB" + uploadedSize="0.2MB" + isUploading={!uploadStatus[file.name]} + /> + ))} +
+
+
+ + + ); }; diff --git a/src/page/archiving/index/component/TimeBlockModal/constant/iconBlock.tsx b/src/page/archiving/index/component/TimeBlockModal/constant/iconBlock.tsx index ea03ef68b..b1229a0e5 100644 --- a/src/page/archiving/index/component/TimeBlockModal/constant/iconBlock.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/constant/iconBlock.tsx @@ -1,40 +1,35 @@ import { ReactNode } from 'react'; -import Accounting from '@/common/asset/svg/ic_accounting.svg?react'; -import Event from '@/common/asset/svg/ic_event.svg?react'; -import Meeting from '@/common/asset/svg/ic_meeting.svg?react'; -import Notice from '@/common/asset/svg/ic_notice.svg?react'; -import Study from '@/common/asset/svg/ic_study.svg?react'; -import Task from '@/common/asset/svg/ic_task.svg?react'; +import Event from '@/common/asset/svg/ic_event_gray.svg?react'; +import Meeting from '@/common/asset/svg/ic_meeting_gray.svg?react'; +import Notice from '@/common/asset/svg/ic_notice_gray.svg?react'; +import Recruiting from '@/common/asset/svg/ic_recruiting_gray.svg?react'; +import Study from '@/common/asset/svg/ic_study_gray.svg?react'; -type BlockIconType = { +type BlockIcon = { name: string; icon: ReactNode; }; -export const BLOCK_ICON: BlockIconType[] = [ +export const BLOCK_ICON: BlockIcon[] = [ { name: 'MEETING', - icon: , + icon: , }, { - name: 'ACCOUNTING', - icon: , + name: 'EVENT', + icon: , }, { - name: 'TASK', - icon: , + name: 'STUDY', + icon: , }, { name: 'NOTICE', - icon: , - }, - { - name: 'STUDY', - icon: , + icon: , }, { - name: 'EVENT', - icon: , + name: 'RECRUITING', + icon: , }, ]; diff --git a/src/page/archiving/index/component/TimeBlockModal/hook/common/useDateRange.tsx b/src/page/archiving/index/component/TimeBlockModal/hook/common/useDateRange.tsx deleted file mode 100644 index 526747e8f..000000000 --- a/src/page/archiving/index/component/TimeBlockModal/hook/common/useDateRange.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from 'react'; - -import { ERROR } from '@/page/archiving/index/component/TimeBlockModal/constant/error'; -import { formatDateString, isValidDate, parseDate } from '@/page/archiving/index/component/TimeBlockModal/util/date'; - -const useDateRange = ( - initialStartDate = '', - initialEndDate = '', - onSetStartDate: (date: string) => void, - onSetEndDate: (date: string) => void, - onSetIsDateRangeValid: (isValid: boolean) => void -) => { - const [dates, setDates] = useState({ startDate: initialStartDate, endDate: initialEndDate }); - const [validation, setValidation] = useState({ - isStartDateValid: true, - isEndDateValid: true, - isStartDateError: false, - isEndDateError: false, - errorMessage: '', - }); - const [isDateRangeValid, setIsDateRangeValid] = useState(false); - - const handleChange = (dateType: 'startDate' | 'endDate', value: string, otherIsValid: boolean, isStart: boolean) => { - const input = value.replace(/\D/g, ''); - const formattedDate = input.length <= 8 ? formatDateString(input) : input; - setDates((prev) => { - const newDates = { ...prev, [dateType]: formattedDate }; - onSetStartDate(newDates.startDate); - onSetEndDate(newDates.endDate); - return newDates; - }); - - const isValid = input.length === 8 && isValidDate(formattedDate); - setValidation((prev) => ({ - ...prev, - [isStart ? 'isStartDateValid' : 'isEndDateValid']: isValid, - })); - - if (!input) { - setValidation((prev) => ({ - ...prev, - [isStart ? 'isStartDateError' : 'isEndDateError']: true, - errorMessage: isStart ? ERROR.START : ERROR.END, - })); - setIsDateRangeValid(false); - onSetIsDateRangeValid(false); - } else if (formattedDate.length === 10) { - if (!isValid || !otherIsValid) { - setValidation((prev) => ({ - ...prev, - [isStart ? 'isStartDateError' : 'isEndDateError']: true, - errorMessage: ERROR.OTHER, - })); - setIsDateRangeValid(false); - onSetIsDateRangeValid(false); - } else { - checkDateRange(formattedDate, dates.startDate, dates.endDate, isStart); - } - } else { - setValidation((prev) => ({ - ...prev, - [isStart ? 'isStartDateError' : 'isEndDateError']: false, - errorMessage: '', - })); - setIsDateRangeValid(false); - onSetIsDateRangeValid(false); - } - }; - - const checkDateRange = (formattedDate: string, startDate: string, endDate: string, isStart: boolean) => { - const start = parseDate(isStart ? formattedDate : startDate); - const end = parseDate(isStart ? endDate : formattedDate); - if (start && end) { - const isValidRange = start <= end; - setIsDateRangeValid(isValidRange); - onSetIsDateRangeValid(isValidRange); - if (!isValidRange) { - setValidation({ - isStartDateValid: true, - isEndDateValid: true, - isStartDateError: isStart, - isEndDateError: !isStart, - errorMessage: ERROR.OTHER, - }); - } else { - setValidation({ - isStartDateValid: true, - isEndDateValid: true, - isStartDateError: false, - isEndDateError: false, - errorMessage: '', - }); - } - } else { - setIsDateRangeValid(true); - onSetIsDateRangeValid(true); - setValidation({ - isStartDateValid: true, - isEndDateValid: true, - isStartDateError: false, - isEndDateError: false, - errorMessage: '', - }); - } - }; - - return { - dates, - validation, - isDateRangeValid, - handleChange, - }; -}; - -export default useDateRange; diff --git a/src/page/archiving/index/component/TimeBlockModal/index.tsx b/src/page/archiving/index/component/TimeBlockModal/index.tsx index d53868f48..ecf2827aa 100644 --- a/src/page/archiving/index/component/TimeBlockModal/index.tsx +++ b/src/page/archiving/index/component/TimeBlockModal/index.tsx @@ -1,15 +1,22 @@ import BlockModal from '@/page/archiving/index/component/TimeBlockModal/component/Block/BlockModal'; import UploadModal from '@/page/archiving/index/component/TimeBlockModal/component/Upload/UploadModal'; -import { useBlockContext } from '@/shared/hook/common/useBlockContext'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; +import { FunnelStep } from '@/shared/util/funnelStep'; export const BlockFlow = () => { - const { step } = useBlockContext(); + const { setTotalSteps } = useFunnel(); + + setTotalSteps(2); return ( <> - - + + + + + + ); }; diff --git a/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.style.ts b/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.style.ts new file mode 100644 index 000000000..df9c1bbbe --- /dev/null +++ b/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.style.ts @@ -0,0 +1,59 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const iconBackStyle = css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + width: '4rem', + height: '4rem', + + borderRadius: '50%', + backgroundColor: theme.colors.gray_100, +}); + +export const textStyle = css({ + color: theme.colors.gray_800, +}); + +export const tagStyle = (backgroundColor: string) => + css({ + padding: '0.4rem 0.8rem', + borderRadius: '10px', + fontWeight: 500, + + backgroundColor: (() => { + switch (backgroundColor) { + case '#D3EFFA': + return theme.colors.sky_100; + case '#F8E1F5': + return theme.colors.pink_100; + case '#F8E2CB': + return theme.colors.yellow_100; + case '#DCD8FA': + return theme.colors.purple_100; + case '#C4F2E5': + return theme.colors.green_100; + default: + return theme.colors.gray_100; + } + })(), + color: (() => { + switch (backgroundColor) { + case '#D3EFFA': + return theme.colors.sky_200; + case '#F8E1F5': + return theme.colors.pink_200; + case '#F8E2CB': + return theme.colors.yellow_200; + case '#DCD8FA': + return theme.colors.purple_200; + case '#C4F2E5': + return theme.colors.green_200; + default: + return theme.colors.gray_200; + } + })(), + }); diff --git a/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.tsx b/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.tsx new file mode 100644 index 000000000..a2a1eeecf --- /dev/null +++ b/src/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.tsx @@ -0,0 +1,61 @@ +import CalenderIcon from '@/common/asset/svg/ic_calendar_tag.svg?react'; +import Delete from '@/common/asset/svg/ic_invite_delete.svg?react'; +import Flex from '@/common/component/Flex/Flex'; +import Text from '@/common/component/Text/Text'; + +import { + iconBackStyle, + tagStyle, + textStyle, +} from '@/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem.style'; +import { TAG_ICON } from '@/shared/constant/icon'; + +interface ActivityTagItemProps { + title: string; + date: string; + tag: string; + color: string; + onDelete: () => void; +} + +const ActivityTagItem = ({ title, date, tag, color, onDelete }: ActivityTagItemProps) => { + const icon = TAG_ICON.find((iconItem) => iconItem.name === tag)?.icon; + + return ( + + +
{icon}
+ + {title} + + + + {date} + + + +
+ + + + {tag.toLowerCase()} + + + +
+ ); +}; + +export default ActivityTagItem; diff --git a/src/shared/component/ActivityTagModal/ActivityTagModal.tsx b/src/shared/component/ActivityTagModal/ActivityTagModal.tsx new file mode 100644 index 000000000..c60dc2a50 --- /dev/null +++ b/src/shared/component/ActivityTagModal/ActivityTagModal.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import SearchIcon from '@/common/asset/svg/ic_search.svg?react'; +import Flex from '@/common/component/Flex/Flex'; +import Input from '@/common/component/Input/Input'; +import { Modal } from '@/common/component/Modal'; +import Text from '@/common/component/Text/Text'; + +import ActivityTagItem from '@/shared/component/ActivityTagModal/ActivityTagItem/ActivityTagItem'; +import { scrollStyle, textStyle } from '@/shared/component/InviteModal/InviteModal.style'; +import { ACITIVITY_TAG_DATA } from '@/shared/constant'; +import { useCloseModal } from '@/shared/store/modal'; + +const ActivityTagModal = () => { + const [inputValue, setInputValue] = useState(''); + + const [activityTags, setActivityTags] = useState(() => [...ACITIVITY_TAG_DATA]); + + const closeModal = useCloseModal(); + + /* 드롭다운 검색 기능 추가할때 버튼활성화 조건 바꾸기! */ + const isButtonActive = inputValue.trim().length > 0; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleDeleteActivityTag = (id: number) => { + setActivityTags((prevTags) => prevTags.filter((tag) => tag.id !== id)); + }; + + return ( + <> + + + + } + value={inputValue} + onChange={handleInputChange} + /> +
+ {activityTags.length > 0 ? ( + activityTags.map((data) => ( + handleDeleteActivityTag(data.id)} + /> + )) + ) : ( + + 태그된 활동이 없습니다. + + )} +
+
+
+ + + ); +}; + +export default ActivityTagModal; diff --git a/src/shared/component/DeleteModal/DeleteModal.style.ts b/src/shared/component/DeleteModal/DeleteModal.style.ts deleted file mode 100644 index e18d27832..000000000 --- a/src/shared/component/DeleteModal/DeleteModal.style.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { css } from '@emotion/react'; - -import { theme } from '@/common/style/theme/theme'; - -export const deleteStyle = css({ - '&:hover': { - backgroundColor: theme.colors.key_200, - }, - - width: '15.8rem', -}); - -export const cancelStyle = css({ - '&:hover': { - backgroundColor: theme.colors.blue_100, - }, - - width: '15.8rem', -}); diff --git a/src/shared/component/DeleteModal/DeleteModal.tsx b/src/shared/component/DeleteModal/DeleteModal.tsx deleted file mode 100644 index 4595630e0..000000000 --- a/src/shared/component/DeleteModal/DeleteModal.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; -import Heading from '@/common/component/Heading/Heading'; -import Text from '@/common/component/Text/Text'; - -import { useDeleteBlockMutation } from '@/page/archiving/index/hook/api/useDeleteBlockMutaion'; -import { useDeleteDocumentMutation } from '@/page/archiving/index/hook/api/useDeleteDocumentMutation'; - -import { cancelStyle, deleteStyle } from '@/shared/component/DeleteModal/DeleteModal.style'; -import { DELETE_DETAIL, DELETE_TITLE } from '@/shared/constant'; -import { useCloseModal, useModalData } from '@/shared/store/modal'; - -const DeleteModal = () => { - const modalData = useModalData(); - const closeModal = useCloseModal(); - - const { mutate: blockMutate } = useDeleteBlockMutation(); - const { mutate: documentMutate } = useDeleteDocumentMutation(); - - if (!modalData) { - return null; - } - - const { teamId, itemId, itemType } = modalData; - - const handleDeleteBlock = () => { - if (teamId && itemId) { - blockMutate( - { teamId, blockId: itemId }, - { - onSuccess: () => { - closeModal(); - }, - } - ); - } - }; - - const handleDeleteDocs = () => { - if (teamId && itemId) { - documentMutate( - { teamId, documentId: itemId }, - { - onSuccess: () => { - closeModal(); - }, - } - ); - } - }; - - const handleDelete = itemType === 'block' ? handleDeleteBlock : handleDeleteDocs; - - if (!itemType || !teamId || !itemId) return null; - - return ( - - - {DELETE_TITLE[itemType.toUpperCase() as keyof typeof DELETE_TITLE]} - - - {DELETE_DETAIL[itemType.toUpperCase() as keyof typeof DELETE_DETAIL]} - - - - - - - - ); -}; - -export default DeleteModal; diff --git a/src/shared/component/DeleteModal/DeletedModal.style.ts b/src/shared/component/DeleteModal/DeletedModal.style.ts new file mode 100644 index 000000000..1a08c12bc --- /dev/null +++ b/src/shared/component/DeleteModal/DeletedModal.style.ts @@ -0,0 +1,13 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const titleStyle = css({ + color: theme.colors.gray_800, + fontWeight: 600, +}); + +export const detailStyle = css({ + color: theme.colors.gray_800, + fontWeight: 500, +}); diff --git a/src/shared/component/DeleteModal/DeletedModal.tsx b/src/shared/component/DeleteModal/DeletedModal.tsx new file mode 100644 index 000000000..b5ed7594b --- /dev/null +++ b/src/shared/component/DeleteModal/DeletedModal.tsx @@ -0,0 +1,42 @@ +import Flex from '@/common/component/Flex/Flex'; +import { Modal } from '@/common/component/Modal'; +import Text from '@/common/component/Text/Text'; + +import { detailStyle, titleStyle } from '@/shared/component/DeleteModal/DeletedModal.style'; +import { DELETED_DETAIL, DELETED_TITLE } from '@/shared/constant'; +import { useCloseModal, useModalData } from '@/shared/store/modal'; + +const DeletedModal = () => { + const closeModal = useCloseModal(); + const modalData = useModalData(); + + const itemType = modalData?.itemType; + const title = itemType && DELETED_TITLE[itemType.toUpperCase() as keyof typeof DELETED_TITLE]; + + const handleDelete = () => { + //api 로직 추가하기 + + closeModal(); + }; + + if (!itemType) return null; + + return ( + <> + + + + + {title} + + + {DELETED_DETAIL} + + + + + + ); +}; + +export default DeletedModal; diff --git a/src/shared/component/InviteModal/InviteModal.style.ts b/src/shared/component/InviteModal/InviteModal.style.ts new file mode 100644 index 000000000..02a237d37 --- /dev/null +++ b/src/shared/component/InviteModal/InviteModal.style.ts @@ -0,0 +1,50 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const inputWrapperStyle = css({ + maxWidth: '24.7rem', + height: '4rem', +}); + +export const textStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + width: '100%', + height: '100%', + + color: theme.colors.gray_800, + textAlign: 'center', +}); + +export const scrollStyle = css({ + display: 'flex', + flexDirection: 'column', + + height: '30.4rem', + width: '100%', + + gap: '1.6rem', + position: 'relative', + + overflowY: 'auto', + overflowX: 'hidden', + + boxSizing: 'content-box', + + '&::-webkit-scrollbar': { + width: '0.4rem', + height: '5rem', + }, + + '&::-webkit-scrollbar-thumb': { + backgroundColor: theme.colors.gray_300, + borderRadius: '4px', + }, + + '&::-webkit-scrollbar-track': { + backgroundColor: 'transparent', + }, +}); diff --git a/src/shared/component/InviteModal/InviteModal.tsx b/src/shared/component/InviteModal/InviteModal.tsx new file mode 100644 index 000000000..c2feb4590 --- /dev/null +++ b/src/shared/component/InviteModal/InviteModal.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; + +import CommandButton from '@/common/component/CommandButton/CommandButton'; +import Flex from '@/common/component/Flex/Flex'; +import Input from '@/common/component/Input/Input'; +import { Modal } from '@/common/component/Modal'; +import Text from '@/common/component/Text/Text'; + +import { inputWrapperStyle, scrollStyle, textStyle } from '@/shared/component/InviteModal/InviteModal.style'; +import MemberItem from '@/shared/component/InviteModal/Member/MemberItem'; +import { useCloseModal } from '@/shared/store/modal'; + +const InviteModal = () => { + const [inputValue, setInputValue] = useState(''); + const [inviteList, setInviteList] = useState([]); + + const closeModal = useCloseModal(); + + const isButtonActive = inputValue.trim().length > 0; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleAddInvite = () => { + if (inputValue.trim() && !inviteList.includes(inputValue)) { + setInviteList([...inviteList, inputValue.trim()]); + setInputValue(''); + } + }; + + const handleDeleteInvite = (email: string) => { + setInviteList(inviteList.filter((item) => item !== email)); + }; + + return ( + <> + + + + + + + 추가 + + + +
+ {inviteList.length > 0 ? ( + inviteList.map((email) => ( + handleDeleteInvite(email)} /> + )) + ) : ( + + 초대된 팀원이 없습니다. + + )} +
+
+
+ + + ); +}; + +export default InviteModal; diff --git a/src/shared/component/InviteModal/Member/MemberItem.style.ts b/src/shared/component/InviteModal/Member/MemberItem.style.ts new file mode 100644 index 000000000..9570d6196 --- /dev/null +++ b/src/shared/component/InviteModal/Member/MemberItem.style.ts @@ -0,0 +1,7 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const textStyle = css({ + color: theme.colors.gray_800, +}); diff --git a/src/shared/component/InviteModal/Member/MemberItem.tsx b/src/shared/component/InviteModal/Member/MemberItem.tsx new file mode 100644 index 000000000..0bba93b0d --- /dev/null +++ b/src/shared/component/InviteModal/Member/MemberItem.tsx @@ -0,0 +1,39 @@ +import Delete from '@/common/asset/svg/ic_invite_delete.svg?react'; +import InviteProfile from '@/common/asset/svg/ic_invite_profile.svg?react'; +import Flex from '@/common/component/Flex/Flex'; +import Text from '@/common/component/Text/Text'; + +import { textStyle } from '@/shared/component/InviteModal/Member/MemberItem.style'; + +interface MemberItemProps { + title: string; + onDelete: () => void; +} + +const MemberItem = ({ title, onDelete }: MemberItemProps) => { + return ( + + + + + {title} + + + + + + ); +}; + +export default MemberItem; diff --git a/src/shared/component/MemberTagModal/MemberTagItem/MemberTagItem.tsx b/src/shared/component/MemberTagModal/MemberTagItem/MemberTagItem.tsx new file mode 100644 index 000000000..493c3854d --- /dev/null +++ b/src/shared/component/MemberTagModal/MemberTagItem/MemberTagItem.tsx @@ -0,0 +1,52 @@ +import Delete from '@/common/asset/svg/ic_invite_delete.svg?react'; +import Profile from '@/common/asset/svg/ic_invite_profile.svg?react'; +import Flex from '@/common/component/Flex/Flex'; +import Text from '@/common/component/Text/Text'; + +import { textStyle } from '@/shared/component/InviteModal/Member/MemberItem.style'; + +interface MemberTagItemProps { + name: string; + email: string; + profileImg?: string; + onDelete: () => void; +} + +const MemberTagItem = ({ name, email, profileImg, onDelete }: MemberTagItemProps) => { + return ( + + + {profileImg ? ( + {`${name} + ) : ( + + )} + + {name} + + {email} + + + + + + + ); +}; + +export default MemberTagItem; diff --git a/src/shared/component/MemberTagModal/MemberTagModal.tsx b/src/shared/component/MemberTagModal/MemberTagModal.tsx new file mode 100644 index 000000000..d26ce54b5 --- /dev/null +++ b/src/shared/component/MemberTagModal/MemberTagModal.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; + +import SearchIcon from '@/common/asset/svg/ic_search.svg?react'; +import Flex from '@/common/component/Flex/Flex'; +import Input from '@/common/component/Input/Input'; +import { Modal } from '@/common/component/Modal'; +import Text from '@/common/component/Text/Text'; + +import { scrollStyle, textStyle } from '@/shared/component/InviteModal/InviteModal.style'; +import MemberTagItem from '@/shared/component/MemberTagModal/MemberTagItem/MemberTagItem'; +import { MEMBER_DATA } from '@/shared/constant'; +import { useCloseModal } from '@/shared/store/modal'; + +const MemberTagModal = () => { + const [inputValue, setInputValue] = useState(''); + const [memberTagList, setMemberTagList] = useState(MEMBER_DATA.map((member) => ({ ...member }))); + + const closeModal = useCloseModal(); + + /* 드롭다운 검색 기능 구현할때 버튼활성화 조건 변경하기 */ + const isButtonActive = inputValue.trim().length > 0; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + }; + + const handleDeleteMemberTag = (email: string) => { + setMemberTagList(memberTagList.filter((item) => item.email !== email)); + }; + + return ( + <> + + + + } + value={inputValue} + onChange={handleInputChange} + /> +
+ {memberTagList.length > 0 ? ( + memberTagList.map((data) => ( + handleDeleteMemberTag(data.email)} + /> + )) + ) : ( + + 태그된 팀원이 없습니다. + + )} +
+
+
+ + + ); +}; + +export default MemberTagModal; diff --git a/src/shared/component/Modal/Body/ModalBody.tsx b/src/shared/component/Modal/Body/ModalBody.tsx new file mode 100644 index 000000000..fc4a729a1 --- /dev/null +++ b/src/shared/component/Modal/Body/ModalBody.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from 'react'; + +import Flex from '@/common/component/Flex/Flex'; + +interface ModalBodyProps { + children: ReactNode; +} + +const ModalBody = ({ children }: ModalBodyProps) => ( + + {children} + +); + +export default ModalBody; diff --git a/src/shared/component/Modal/Footer/ModalFooter.tsx b/src/shared/component/Modal/Footer/ModalFooter.tsx new file mode 100644 index 000000000..8565068a9 --- /dev/null +++ b/src/shared/component/Modal/Footer/ModalFooter.tsx @@ -0,0 +1,39 @@ +import Button from '@/common/component/Button/Button'; +import Flex from '@/common/component/Flex/Flex'; + +import { MODAL_CONTENTS, isModalContentType } from '@/shared/constant/modal'; +import { useCloseModal, useModalContentType } from '@/shared/store/modal'; + +interface ModalFooterProps { + step?: number; + buttonClick?: () => void; + isButtonActive?: boolean; +} + +const ModalFooter = ({ step = 1, buttonClick, isButtonActive = true }: ModalFooterProps) => { + const contentType = useModalContentType(); + const closeModal = useCloseModal(); + + if (!isModalContentType(contentType)) return null; + + const modalContent = MODAL_CONTENTS[contentType]; + const buttons = modalContent.buttons[step - 1]; + + return ( + + {buttons.map((button: (typeof buttons)[number], index: number) => ( + + ))} + + ); +}; + +export default ModalFooter; diff --git a/src/shared/component/Modal/Header/ModalHeader.style.ts b/src/shared/component/Modal/Header/ModalHeader.style.ts new file mode 100644 index 000000000..f83d4af51 --- /dev/null +++ b/src/shared/component/Modal/Header/ModalHeader.style.ts @@ -0,0 +1,21 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const infoTextStyle = css({ + color: theme.colors.gray_800, +}); + +export const iconTextStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + width: '4rem', + height: '4rem', + + fontWeight: 500, + color: theme.colors.gray_800, + + textAlign: 'center', +}); diff --git a/src/shared/component/Modal/Header/ModalHeader.tsx b/src/shared/component/Modal/Header/ModalHeader.tsx new file mode 100644 index 000000000..68b818f7e --- /dev/null +++ b/src/shared/component/Modal/Header/ModalHeader.tsx @@ -0,0 +1,44 @@ +import Flex from '@/common/component/Flex/Flex'; +import Text from '@/common/component/Text/Text'; + +import { iconTextStyle, infoTextStyle } from '@/shared/component/Modal/Header/ModalHeader.style'; +import { MODAL_CONTENTS, isModalContentType } from '@/shared/constant/modal'; +import { useModalContentType } from '@/shared/store/modal'; + +interface ModalHeaderProps { + step?: number; + totalSteps?: number; +} + +const ModalHeader = ({ step = 1, totalSteps = 4 }: ModalHeaderProps) => { + const contentType = useModalContentType(); + + if (!isModalContentType(contentType)) return null; + + const modalContent = MODAL_CONTENTS[contentType]; + const { icon, title, infoText } = modalContent.headers[step - 1]; + + const displayIcon = typeof icon === 'function' ? icon(step, totalSteps) : icon; // 함수인지 확인 후 호출 + + return ( + + {displayIcon && ( + + {displayIcon} + + )} + + + {title} + + {infoText && ( + + {infoText} + + )} + + + ); +}; + +export default ModalHeader; diff --git a/src/shared/component/Modal/ModalContainer.tsx b/src/shared/component/Modal/ModalContainer.tsx deleted file mode 100644 index 97978320b..000000000 --- a/src/shared/component/Modal/ModalContainer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Modal from '@/common/component/Modal/Modal'; - -import { BlockFlow } from '@/page/archiving/index/component/TimeBlockModal'; - -import DeleteModal from '@/shared/component/DeleteModal/DeleteModal'; -import { WorkSpaceFlow } from '@/shared/component/WorkSpaceModal'; -import { BlockProvider } from '@/shared/hook/common/useBlockContext'; -import { WorkSpaceProvider } from '@/shared/hook/common/useWorkSpaceContext'; -import { useCloseModal, useModalContentType, useModalIsOpen } from '@/shared/store/modal'; - -const ModalContainer = () => { - const isOpen = useModalIsOpen(); - const contentType = useModalContentType(); - const closeModal = useCloseModal(); - - const renderContent = () => { - switch (contentType) { - case 'create-workspace': - return ( - - - - ); - case 'create-block': - return ( - - - - ); - case 'delete': - return ; - default: - return null; - } - }; - - if (!isOpen || !contentType) return null; - - return ( - - {renderContent()} - - ); -}; - -export default ModalContainer; diff --git a/src/shared/component/Modal/ModalFunnel.tsx b/src/shared/component/Modal/ModalFunnel.tsx new file mode 100644 index 000000000..9b49af405 --- /dev/null +++ b/src/shared/component/Modal/ModalFunnel.tsx @@ -0,0 +1,73 @@ +import { Modal } from '@/common/component/Modal'; + +import { BlockFlow } from '@/page/archiving/index/component/TimeBlockModal'; + +import ActivityTagModal from '@/shared/component/ActivityTagModal/ActivityTagModal'; +import DeletedModal from '@/shared/component/DeleteModal/DeletedModal'; +import InviteModal from '@/shared/component/InviteModal/InviteModal'; +import MemberTagModal from '@/shared/component/MemberTagModal/MemberTagModal'; +import { WorkSpaceFlow } from '@/shared/component/WorkSpaceModal/index'; +import { FunnelProvider } from '@/shared/hook/common/funnelContext'; +import { BlockProvider } from '@/shared/hook/common/useBlockContext'; +import { WorkSpaceProvider } from '@/shared/hook/common/useWorkSpaceContext'; +import { useCloseModal, useModalContentType, useModalIsOpen } from '@/shared/store/modal'; +import { FunnelStep } from '@/shared/util/funnelStep'; + +const ModalFunnel = () => { + const isOpen = useModalIsOpen(); + const contentType = useModalContentType(); + const closeModal = useCloseModal(); + + const renderContent = () => { + switch (contentType) { + case 'create-workspace': + return ( + + + + ); + case 'create-block': + return ( + + + + ); + case 'deleted': + return ( + + + + ); + case 'invite': + return ( + + + + ); + case 'member-tag': + return ( + + + + ); + case 'activity-tag': + return ( + + + + ); + default: + return null; + } + }; + + if (!isOpen) return null; + + return ( + + {renderContent()} + + ); +}; + +export default ModalFunnel; diff --git a/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx b/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx index acf483013..055d66442 100644 --- a/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx +++ b/src/shared/component/WorkSpaceModal/category/WorkSpaceCategory.tsx @@ -1,23 +1,18 @@ import { useEffect, useState } from 'react'; -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; +import { Modal } from '@/common/component/Modal'; import Select from '@/common/component/Select/Select'; import { useOutsideClick, useOverlay } from '@/common/hook'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; -import { buttonStyle, sectionStyle } from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style'; import useCategoryListQuery from '@/shared/hook/api/useCategoryListQuery'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; -interface WorkSpaceCategoryProps { - isVisible: boolean; -} - -const WorkSpaceCategory = ({ isVisible }: WorkSpaceCategoryProps) => { +const WorkSpaceCategory = () => { const { isOpen, close, toggle } = useOverlay(); - const { setFormData, nextStep } = useWorkSpaceContext(); + const { setFormData } = useWorkSpaceContext(); + const { nextStep } = useFunnel(); const ref = useOutsideClick(close); @@ -47,8 +42,6 @@ const WorkSpaceCategory = ({ isVisible }: WorkSpaceCategoryProps) => { }; }, [isOpen, close, ref]); - if (!isVisible) return null; - const handleSelect = (id: string) => { setSelected(id); setFormData({ category: id }); @@ -62,33 +55,28 @@ const WorkSpaceCategory = ({ isVisible }: WorkSpaceCategoryProps) => { const isButtonActive = selected.trim().length > 0; return ( - - -
- ({ value: str }))} + className="select-container" + /> +
+ + + ); }; diff --git a/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx b/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx index 4f63b5f1b..ec335749f 100644 --- a/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx +++ b/src/shared/component/WorkSpaceModal/complete/WorkSpaceComplete.tsx @@ -2,32 +2,24 @@ import { css } from '@emotion/react'; import completePng from '@/common/asset/img/workspace_complete.png'; import complete from '@/common/asset/img/workspace_complete.webp'; -import Flex from '@/common/component/Flex/Flex'; +import { Modal } from '@/common/component/Modal'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; -import { sectionStyle } from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style'; +import { useCloseModal } from '@/shared/store/modal'; -interface WorkSpaceCompleteProps { - isVisible: boolean; -} - -const WorkSpaceComplete = ({ isVisible }: WorkSpaceCompleteProps) => { - if (!isVisible) return null; +const WorkSpaceComplete = () => { + const closeModal = useCloseModal(); return ( - - - - - 워크 스페이스 완료 이미지 - - + <> + + + + + 워크 스페이스 완료 이미지 + + + + ); }; diff --git a/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.style.ts b/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.style.ts index 9ed868ce5..22bbd43b3 100644 --- a/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.style.ts +++ b/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.style.ts @@ -9,13 +9,15 @@ export const imageBoxStyle = css({ }); export const imageAddStyle = css({ - width: '20rem', - height: '20rem', + width: '23.4rem', + height: '23.4rem', - borderRadius: '57.14px', + borderRadius: '40px', objectFit: 'cover', cursor: 'pointer', + + padding: '1rem', }); export const imageDeleteStyle = css({ @@ -23,8 +25,8 @@ export const imageDeleteStyle = css({ top: '0', right: '0', - width: '3.3rem', - height: '3.2rem', + width: '4.2rem', + height: '4.2rem', cursor: 'pointer', }); diff --git a/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx b/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx index 0b5a81176..81965ff0e 100644 --- a/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx +++ b/src/shared/component/WorkSpaceModal/image/WorkSpaceImage.tsx @@ -1,29 +1,23 @@ import TeamProfileAdd from '@/common/asset/svg/ic_team_profile_add.svg?react'; import TeamProfileDelete from '@/common/asset/svg/ic_team_profile_delete.svg?react'; -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; import Label from '@/common/component/Label/Label'; +import { Modal } from '@/common/component/Modal'; import { - buttonCompleteStyle, imageAddStyle, imageBoxStyle, imageDeleteStyle, } from '@/shared/component/WorkSpaceModal/image/WorkSpaceImage.style'; import useImageUpload from '@/shared/component/WorkSpaceModal/image/hook/useImageUpload'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; -import { sectionStyle } from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style'; import { usePostTeamMutation } from '@/shared/hook/api/usePostTeamMutation'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; -interface WorkSpaceImageProps { - isVisible: boolean; -} - -const WorkSpaceImage = ({ isVisible }: WorkSpaceImageProps) => { +const WorkSpaceImage = () => { const { fileURL, imgUploadInput, handleImageChange, handleImageRemove } = useImageUpload(); - const { nextStep, formData } = useWorkSpaceContext(); + const { formData } = useWorkSpaceContext(); const { mutate: postTeamMutate } = usePostTeamMutation(); + const { nextStep } = useFunnel(); const handleSave = () => { postTeamMutate( @@ -40,37 +34,33 @@ const WorkSpaceImage = ({ isVisible }: WorkSpaceImageProps) => { ); }; - if (!isVisible) return null; + const isButtonActive = !!fileURL; return ( - - -
- {fileURL ? ( - 프로필 이미지 - ) : ( - - )} - {fileURL && } -
- - -
+ <> + + +
+ {fileURL ? ( + 프로필 이미지 + ) : ( + + )} + {fileURL && } +
+ +
+ + ); }; diff --git a/src/shared/component/WorkSpaceModal/index.tsx b/src/shared/component/WorkSpaceModal/index.tsx index 25598aaa1..0e41e6057 100644 --- a/src/shared/component/WorkSpaceModal/index.tsx +++ b/src/shared/component/WorkSpaceModal/index.tsx @@ -2,17 +2,28 @@ import WorkSpaceCategory from '@/shared/component/WorkSpaceModal/category/WorkSp import WorkSpaceComplete from '@/shared/component/WorkSpaceModal/complete/WorkSpaceComplete'; import WorkSpaceImage from '@/shared/component/WorkSpaceModal/image/WorkSpaceImage'; import WorkSpaceName from '@/shared/component/WorkSpaceModal/name/WorkSpaceName'; -import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; +import { FunnelStep } from '@/shared/util/funnelStep'; export const WorkSpaceFlow = () => { - const { step } = useWorkSpaceContext(); + const { setTotalSteps } = useFunnel(); + + setTotalSteps(4); return ( <> - - - - + + + + + + + + + + + + ); }; diff --git a/src/shared/component/WorkSpaceModal/name/WorkSpaceName.tsx b/src/shared/component/WorkSpaceModal/name/WorkSpaceName.tsx index 3ed9617ed..24c49a277 100644 --- a/src/shared/component/WorkSpaceModal/name/WorkSpaceName.tsx +++ b/src/shared/component/WorkSpaceModal/name/WorkSpaceName.tsx @@ -1,26 +1,20 @@ import { useState } from 'react'; -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; import Input from '@/common/component/Input/Input'; +import { Modal } from '@/common/component/Modal'; -import WorkSapceInfo from '@/shared/component/WorkSpaceModal/info/WorkSpaceInfo'; -import { - buttonStyle, - inputWrapperStyle, - sectionStyle, -} from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style'; +import { inputWrapperStyle } from '@/shared/component/WorkSpaceModal/name/WorkSpaceName.style'; +import { useFunnel } from '@/shared/hook/common/funnelContext'; import { useWorkSpaceContext } from '@/shared/hook/common/useWorkSpaceContext'; -interface WorkSpaceNameProps { - isVisible: boolean; -} - -const WorkSpaceName = ({ isVisible }: WorkSpaceNameProps) => { +const WorkSpaceName = () => { const [inputValue, setInputValue] = useState(''); - const { setFormData, nextStep } = useWorkSpaceContext(); + const { setFormData } = useWorkSpaceContext(); + + const { nextStep } = useFunnel(); const handleNext = () => { + console.log('Next button clicked'); // 디버깅용 setFormData({ name: inputValue }); nextStep(); }; @@ -31,28 +25,19 @@ const WorkSpaceName = ({ isVisible }: WorkSpaceNameProps) => { setInputValue(e.target.value); }; - if (!isVisible) return null; - return ( - - -
+ <> + + -
- -
+ + + ); }; diff --git a/src/shared/constant/icon.tsx b/src/shared/constant/icon.tsx new file mode 100644 index 000000000..77ab05ff9 --- /dev/null +++ b/src/shared/constant/icon.tsx @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; + +import Event from '@/common/asset/svg/ic_event_gray.svg?react'; +import Meeting from '@/common/asset/svg/ic_meeting_gray.svg?react'; +import Notice from '@/common/asset/svg/ic_notice_gray.svg?react'; +import Recruiting from '@/common/asset/svg/ic_recruiting_gray.svg?react'; +import Study from '@/common/asset/svg/ic_study_gray.svg?react'; + +type TAGIcon = { + name: string; + icon: ReactNode; +}; + +export const TAG_ICON: TAGIcon[] = [ + { + name: 'MEETING', + icon: , + }, + { + name: 'EVENT', + icon: , + }, + { + name: 'STUDY', + icon: , + }, + { + name: 'NOTICE', + icon: , + }, + { + name: 'RECRUITING', + icon: , + }, +]; diff --git a/src/shared/constant/index.ts b/src/shared/constant/index.ts index d8e2ca33c..0f16c7dcf 100644 --- a/src/shared/constant/index.ts +++ b/src/shared/constant/index.ts @@ -23,3 +23,47 @@ export const DELETE_DETAIL = { BLOCK: '삭제된 블록은 복구할 수 없습니다.', DOCS: '삭제된 문서는 복구할 수 없습니다.', } as const; + +export const DELETED_TITLE = { + TRASH: '휴지통을 비우시겠습니까?', + PERMANENT: '파일을 완전히 삭제하시겠습니까?', +} as const; + +export const DELETED_DETAIL = '휴지통에서 지워진 파일은 영구삭제되며 되돌릴 수 없습니다' as const; + +export const MEMBER_DATA = [ + { + name: '이채원', + email: 'cindy1769@daum.net', + profileUrl: 'https://github.com/user-attachments/assets/a9c876cd-9d07-49db-94f1-353e5c4a5ee3', + }, + { + name: '이채원2', + email: 'cindy1769@naver.com', + profileUrl: 'https://github.com/user-attachments/assets/a9c876cd-9d07-49db-94f1-353e5c4a5ee3', + }, +] as const; + +export const ACITIVITY_TAG_DATA = [ + { + id: 1, + tag: 'MEETING', + title: 'UX 스터디', + date: '2024.09.25', + color: '#D3EFFA', + }, + { + id: 2, + tag: 'STUDY', + title: 'UX 스터디', + date: '2024.09.25', + color: '#F8E1F5', + }, + { + id: 3, + tag: 'NOTICE', + title: 'UX 스터디', + date: '2024.09.25', + color: '#C4F2E5', + }, +] as const; diff --git a/src/shared/constant/modal.tsx b/src/shared/constant/modal.tsx new file mode 100644 index 000000000..757815b31 --- /dev/null +++ b/src/shared/constant/modal.tsx @@ -0,0 +1,152 @@ +import ActivityTagIcon from '@/common/asset/svg/ic_activity_tag.svg?react'; +import BlockIcon from '@/common/asset/svg/ic_block_create.svg?react'; +import InviteIcon from '@/common/asset/svg/ic_invite.svg?react'; +import MemberTagIcon from '@/common/asset/svg/ic_member_tag.svg?react'; +import WarningIcon from '@/common/asset/svg/ic_warning.svg?react'; +import SuccessIcon from '@/common/asset/svg/ic_workspace_success.svg?react'; + +type ModalContentType = 'create-workspace' | 'create-block' | 'deleted' | 'invite' | 'member-tag' | 'activity-tag'; + +interface ModalHeader { + icon: React.ReactNode | ((step: number, totalSteps: number) => React.ReactNode); + title: string; + infoText: string; +} + +interface ModalButton { + text: string; + variant: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'underline'; + disabled?: boolean; +} + +interface ModalContent { + steps: number; + headers: ModalHeader[]; + buttons: ModalButton[][]; +} + +export const isModalContentType = (type: string | null): type is ModalContentType => { + return type !== null && type in MODAL_CONTENTS; +}; + +export const MODAL_CONTENTS: Record = { + 'create-workspace': { + steps: 4, + headers: [ + { + icon: (step: number, totalSteps: number) => + step === totalSteps ? : {`${step}/${totalSteps}`}, + title: '워크스페이스 이름 입력', + infoText: '워크스페이스 이름을 입력해주세요.', + }, + { + icon: (step: number, totalSteps: number) => + step === totalSteps ? : {`${step}/${totalSteps}`}, + title: '카테고리 선택', + infoText: '카테고리를 선택해주세요.', + }, + { + icon: (step: number, totalSteps: number) => + step === totalSteps ? : {`${step}/${totalSteps}`}, + title: '프로필 이미지 등록', + infoText: '프로필 이미지를 등록해주세요.', + }, + { + icon: (step: number, totalSteps: number) => + step === totalSteps ? : {`${step}/${totalSteps}`}, + title: '완료', + infoText: '워크스페이스 생성이 완료되었습니다.', + }, + ], + buttons: [ + [ + { text: '건너뛰기', variant: 'outline' }, + { text: '다음으로', variant: 'primary' }, + ], + [ + { text: '건너뛰기', variant: 'outline' }, + { text: '다음으로', variant: 'primary' }, + ], + [ + { text: '건너뛰기', variant: 'outline' }, + { text: '다음으로', variant: 'primary' }, + ], + [{ text: '확인', variant: 'primary' }], + ], + }, + 'create-block': { + steps: 2, + headers: [ + { icon: , title: '타임블록 생성', infoText: '블록 정보를 입력해주세요.' }, + { icon: , title: '파일 업로드', infoText: '파일을 업로드해주세요.' }, + ], + buttons: [ + [ + { text: '취소', variant: 'outline' }, + { text: '다음으로', variant: 'primary' }, + ], + [ + { text: '취소', variant: 'outline' }, + { text: '완료', variant: 'primary' }, + ], + ], + }, + deleted: { + steps: 1, + headers: [{ icon: , title: '삭제 확인', infoText: '정말 삭제하시겠습니까?' }], + buttons: [ + [ + { text: '취소', variant: 'outline' }, + { text: '삭제', variant: 'primary' }, + ], + ], + }, + invite: { + steps: 1, + headers: [ + { + icon: , + title: '팀원 초대', + infoText: '워크스페이스에 팀원을 초대할 수 있습니다.', + }, + ], + buttons: [ + [ + { text: '취소', variant: 'outline' }, + { text: '초대', variant: 'primary' }, + ], + ], + }, + 'member-tag': { + steps: 1, + headers: [ + { + icon: , + title: '팀원 태그', + infoText: '관련된 팀원을 태그할 수 있습니다.', + }, + ], + buttons: [ + [ + { text: '취소', variant: 'outline' }, + { text: '완료', variant: 'primary' }, + ], + ], + }, + 'activity-tag': { + steps: 1, + headers: [ + { + icon: , + title: '활동 태그', + infoText: '타임라인에 저장된 활동을 태그할 수 있습니다.', + }, + ], + buttons: [ + [ + { text: '취소', variant: 'outline' }, + { text: '완료', variant: 'primary' }, + ], + ], + }, +}; diff --git a/src/shared/hook/common/funnelContext.tsx b/src/shared/hook/common/funnelContext.tsx new file mode 100644 index 000000000..dfdd5404b --- /dev/null +++ b/src/shared/hook/common/funnelContext.tsx @@ -0,0 +1,62 @@ +import { ReactNode, createContext, useCallback, useContext, useEffect, useState } from 'react'; + +interface FunnelContextType { + currentStep: number; + totalSteps: number; + nextStep: () => void; + prevStep: () => void; + resetSteps: () => void; + setTotalSteps: (steps: number) => void; +} + +const FunnelContext = createContext(undefined); + +export const useFunnel = () => { + const context = useContext(FunnelContext); + if (!context) { + throw new Error('Error: FunnelContext must be used within a FunnelProvider'); + } + return context; +}; + +export const FunnelProvider = ({ children }: { children: ReactNode }) => { + const [currentStep, setCurrentStep] = useState(1); + const [totalSteps, setTotalStepsState] = useState(1); + + const nextStep = useCallback(() => { + setCurrentStep((prev) => { + const next = prev < totalSteps ? prev + 1 : prev; + console.log('Next step:', next); // 디버깅용 + return next; + }); + }, [totalSteps]); + + const prevStep = useCallback(() => { + setCurrentStep((prev) => { + const prevStep = prev > 1 ? prev - 1 : prev; + console.log('Previous step:', prevStep); // 디버깅용 + return prevStep; + }); + }, []); + + const resetSteps = useCallback(() => { + console.log('Reset steps'); // 디버깅용 + setCurrentStep(1); + }, []); + + const setTotalSteps = useCallback((steps: number) => { + console.log('Setting total steps:', steps); // 디버깅용 + setTotalStepsState(steps); + setCurrentStep(1); + }, []); + + useEffect(() => { + console.log('Current step updated:', currentStep); // 디버깅용 + }, [currentStep]); + + return ( + + {children} + + ); +}; diff --git a/src/shared/hook/common/useBlockContext.tsx b/src/shared/hook/common/useBlockContext.tsx index 5ea4406c0..e24e36a87 100644 --- a/src/shared/hook/common/useBlockContext.tsx +++ b/src/shared/hook/common/useBlockContext.tsx @@ -9,11 +9,9 @@ interface BlockFormData { } interface BlockContextType { - step: number; - nextStep: () => void; - reset: () => void; formData: BlockFormData; setFormData: (data: Partial) => void; + resetFormData: () => void; } const BlockContext = createContext(undefined); @@ -21,13 +19,12 @@ const BlockContext = createContext(undefined); export const useBlockContext = () => { const context = useContext(BlockContext); if (!context) { - throw new Error('Error useBlockContext'); + throw new Error('Error: BlockContext must be used within a BlockProvider'); } return context; }; export const BlockProvider = ({ children }: { children: ReactNode }) => { - const [step, setStep] = useState(1); const [formData, setFormDataState] = useState({ blockName: '', blockType: '', @@ -36,10 +33,11 @@ export const BlockProvider = ({ children }: { children: ReactNode }) => { endDate: '', }); - const nextStep = useCallback(() => setStep((prev) => prev + 1), []); + const setFormData = useCallback((data: Partial) => { + setFormDataState((prev) => ({ ...prev, ...data })); + }, []); - const reset = useCallback(() => { - setStep(1); + const resetFormData = useCallback(() => { setFormDataState({ blockName: '', blockType: '', @@ -49,11 +47,5 @@ export const BlockProvider = ({ children }: { children: ReactNode }) => { }); }, []); - const setFormData = useCallback((data: Partial) => { - setFormDataState((prev) => ({ ...prev, ...data })); - }, []); - - return ( - {children} - ); + return {children}; }; diff --git a/src/shared/hook/common/useWorkSpaceContext.tsx b/src/shared/hook/common/useWorkSpaceContext.tsx index a9e5a4fad..6043c5e8e 100644 --- a/src/shared/hook/common/useWorkSpaceContext.tsx +++ b/src/shared/hook/common/useWorkSpaceContext.tsx @@ -7,11 +7,9 @@ interface WorkSpaceFormData { } interface WorkSpaceContextType { - step: number; - nextStep: () => void; - reset: () => void; formData: WorkSpaceFormData; setFormData: (data: Partial) => void; + resetFormData: () => void; } const WorkSpaceContext = createContext(undefined); @@ -19,23 +17,23 @@ const WorkSpaceContext = createContext(undefin export const useWorkSpaceContext = () => { const context = useContext(WorkSpaceContext); if (!context) { - throw new Error('Error useWorkSpaceContext'); + throw new Error('Error: WorkSpaceContext must be used within a WorkSpaceProvider'); } return context; }; export const WorkSpaceProvider = ({ children }: { children: ReactNode }) => { - const [step, setStep] = useState(1); const [formData, setFormDataState] = useState({ name: '', category: '', fileUrlData: '', }); - const nextStep = useCallback(() => setStep((prev) => prev + 1), []); + const setFormData = useCallback((data: Partial) => { + setFormDataState((prev) => ({ ...prev, ...data })); + }, []); - const reset = useCallback(() => { - setStep(1); + const resetFormData = useCallback(() => { setFormDataState({ name: '', category: '', @@ -43,13 +41,7 @@ export const WorkSpaceProvider = ({ children }: { children: ReactNode }) => { }); }, []); - const setFormData = useCallback((data: Partial) => { - setFormDataState((prev) => ({ ...prev, ...data })); - }, []); - return ( - - {children} - + {children} ); }; diff --git a/src/shared/store/modal.tsx b/src/shared/store/modal.tsx index 4855beff3..08866bcd7 100644 --- a/src/shared/store/modal.tsx +++ b/src/shared/store/modal.tsx @@ -1,17 +1,15 @@ import { create } from 'zustand'; -interface DeleteModalData { - teamId: number; - itemId: number; - itemType: 'block' | 'docs'; +interface DeletedModalData { + itemType: 'trash' | 'permanent'; } interface ModalState { isOpen: boolean; contentType: string | null; - modalData: DeleteModalData | null; + modalData: DeletedModalData | null; actions: { - openModal: (contentType: string, data?: DeleteModalData | null) => void; + openModal: (contentType: string, data?: DeletedModalData | null) => void; closeModal: () => void; }; } @@ -22,9 +20,12 @@ const useModalStore = create((set) => ({ modalData: null, actions: { openModal: (contentType, data = null) => { - set({ isOpen: true, contentType, modalData: data }); + set({ + isOpen: true, + contentType, + modalData: data || null, + }); }, - closeModal: () => set({ isOpen: false, contentType: null, modalData: null }), }, })); diff --git a/src/shared/util/funnelStep.tsx b/src/shared/util/funnelStep.tsx new file mode 100644 index 000000000..2e59f6079 --- /dev/null +++ b/src/shared/util/funnelStep.tsx @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; + +import { useFunnel } from '@/shared/hook/common/funnelContext'; + +interface FunnelStepProps { + step: number; + children: ReactNode; +} + +export const FunnelStep = ({ step, children }: FunnelStepProps) => { + const { currentStep } = useFunnel(); + console.log(`FunnelStep step: ${step}, currentStep: ${currentStep}`); // 디버깅용 + return currentStep === step ? <>{children} : null; +}; diff --git a/src/shared/util/modalFooter.tsx b/src/shared/util/modalFooter.tsx new file mode 100644 index 000000000..0c5517f2a --- /dev/null +++ b/src/shared/util/modalFooter.tsx @@ -0,0 +1,53 @@ +export interface FooterButton { + text: string; + onClick?: () => void; + variant: 'primary' | 'secondary' | 'tertiary' | 'outline' | 'underline'; + disabled?: boolean; +} +export const getFooterContent = ( + contentType: string, + step: number, + buttonClick?: () => void, + closeModal?: () => void, + isButtonActive: boolean = true +): FooterButton[] => { + switch (contentType) { + case 'create-workspace': + return [ + step >= 3 ? { text: '건너뛰기', onClick: buttonClick, variant: 'outline' } : false, + { + text: step === 4 ? '확인' : '다음으로', + onClick: buttonClick, + variant: 'primary', + disabled: !isButtonActive, + }, + ].filter(Boolean) as FooterButton[]; + + /* 디자인 확정시 추후 수정 필요 */ + case 'create-block': + return [ + { text: '취소', onClick: closeModal, variant: 'outline' }, + { text: '다음으로', onClick: buttonClick, variant: 'primary' }, + ]; + + case 'deleted': + return [ + { text: '취소', onClick: closeModal, variant: 'outline' }, + { text: '삭제', onClick: buttonClick, variant: 'primary' }, + ]; + + case 'invite': + return [ + { text: '취소', onClick: closeModal, variant: 'outline' }, + { text: '초대', onClick: buttonClick, variant: 'primary', disabled: !isButtonActive }, + ]; + case 'member-tag': + case 'activity-tag': + return [ + { text: '취소', onClick: closeModal, variant: 'outline' }, + { text: '완료', onClick: buttonClick, variant: 'primary' }, + ]; + default: + return []; + } +}; diff --git a/src/shared/util/modalHeader.tsx b/src/shared/util/modalHeader.tsx new file mode 100644 index 000000000..a3d08f17b --- /dev/null +++ b/src/shared/util/modalHeader.tsx @@ -0,0 +1,69 @@ +import ActivityTagIcon from '@/common/asset/svg/ic_activity_tag.svg?react'; +import BlockIcon from '@/common/asset/svg/ic_block_create.svg?react'; +import InviteIcon from '@/common/asset/svg/ic_invite.svg?react'; +import MemberTagIcon from '@/common/asset/svg/ic_member_tag.svg?react'; +import WarningIcon from '@/common/asset/svg/ic_warning.svg?react'; +import SuccessIcon from '@/common/asset/svg/ic_workspace_success.svg?react'; + +export const getHeaderContent = (contentType: string, step?: number, totalSteps?: number) => { + switch (contentType) { + case 'create-workspace': + return { + icon: + step === totalSteps ? ( + + ) : step && totalSteps ? ( + `${step}/${totalSteps}` + ) : null, + title: + step === 1 || step === 2 + ? '새로운 워크스페이스 생성하기' + : step === 3 + ? '동아리 프로필 이미지 등록' + : '워크스페이스 생성완료', + infoText: + step === 1 + ? '워크스페이스의 이름을 입력해주세요' + : step === 2 + ? '팀 카테고리를 선택해주세요' + : step === 3 + ? '우리 동아리 프로필에 표시할 이미지를 등록해주세요' + : '이제 워크스페이스를 사용할 수 있습니다.', + }; + + /* 디자인 확정시 추후 수정 필요 */ + case 'create-block': + return { + icon: , + title: '타임블록 생성', + infoText: step === 1 ? '타임라인에 생성할 블록 정보를 입력해주세요' : '타임라인에 업로드할 파일을 선택하세요', + }; + + case 'deleted': + return { + icon: , + title: '주의!', + infoText: '삭제할 항목을 확인해주세요.', + }; + case 'invite': + return { + icon: , + title: '팀원 초대', + infoText: '워크스페이스에 팀원을 초대할 수 있습니다.', + }; + case 'member-tag': + return { + icon: , + title: '팀원 태그', + infoText: '관련된 팀원을 태그할 수 있습니다.', + }; + case 'activity-tag': + return { + icon: , + title: '활동 태그', + infoText: '타임라인에 저장된 활동을 태그할 수 있습니다.', + }; + default: + return { icon: null, title: '', infoText: '' }; + } +}; diff --git a/src/story/common/DatePicker.stories.tsx b/src/story/common/DatePicker.stories.tsx index 02c36abfb..99f1d65fb 100644 --- a/src/story/common/DatePicker.stories.tsx +++ b/src/story/common/DatePicker.stories.tsx @@ -1,5 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; + import DatePicker from '@/common/component/DatePicker'; const meta: Meta = { @@ -15,9 +17,48 @@ export default meta; type Story = StoryObj; export const SingleDatePicker: Story = { - render: () => , + render: () => { + const [selectedDate, setSelectedDate] = useState(null); + const [endDate, setEndDate] = useState(null); + + const handleDateChange = (start: Date | null, end: Date | null) => { + setSelectedDate(start); + setEndDate(end); + }; + + return ( + <> + + + ); + }, }; export const RangeDatePicker: Story = { - render: () => , + render: () => { + const [selectedDate, setSelectedDate] = useState(null); + const [endDate, setEndDate] = useState(null); + + const handleDateChange = (start: Date | null, end: Date | null) => { + setSelectedDate(start); + setEndDate(end); + }; + + return ( + <> + + + ); + }, }; diff --git a/src/story/shared/DeleteModal.stories.tsx b/src/story/shared/DeleteModal.stories.tsx index c66b81280..55073b827 100644 --- a/src/story/shared/DeleteModal.stories.tsx +++ b/src/story/shared/DeleteModal.stories.tsx @@ -1,11 +1,11 @@ import { Meta, StoryObj } from '@storybook/react'; -import ModalContainer from '@/shared/component/Modal/ModalContainer'; +import ModalFunnel from '@/shared/component/Modal/ModalFunnel'; import { useOpenModal } from '@/shared/store/modal'; -const meta: Meta = { - title: 'Shared/Modal/Delete', - component: ModalContainer, +const meta: Meta = { + title: 'Shared/Modal/Deleted', + component: ModalFunnel, parameters: { layout: 'centered', }, @@ -22,16 +22,15 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Delete: Story = { +export const Deleted: Story = { render: () => { const openModal = useOpenModal(); return ( <> - - + + + ); }, diff --git a/src/story/shared/NewModals.stories.tsx b/src/story/shared/NewModals.stories.tsx new file mode 100644 index 000000000..862da34d6 --- /dev/null +++ b/src/story/shared/NewModals.stories.tsx @@ -0,0 +1,38 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import ModalFunnel from '@/shared/component/Modal/ModalFunnel'; +import { useOpenModal } from '@/shared/store/modal'; + +const meta: Meta = { + title: 'Shared/Modal/ModalsTest', + component: ModalFunnel, + parameters: { + layout: 'centered', + }, + args: { + isOpen: false, + }, + argTypes: { + children: { + control: false, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const ModalsTest: Story = { + render: () => { + const openModal = useOpenModal(); + + return ( + <> + + + + + + ); + }, +}; diff --git a/src/story/shared/WorkSpaceModal.stories.tsx b/src/story/shared/WorkSpaceModal.stories.tsx deleted file mode 100644 index 67694e04c..000000000 --- a/src/story/shared/WorkSpaceModal.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; - -import WorkSpaceComplete from '@/shared/component/WorkSpaceModal/complete/WorkSpaceComplete'; -import WorkSpaceName from '@/shared/component/WorkSpaceModal/name/WorkSpaceName'; -import { WorkSpaceProvider } from '@/shared/hook/common/useWorkSpaceContext'; - -const meta: Meta = { - title: 'Shared/WorkSpaceModal', - component: WorkSpaceProvider, - parameters: { - layout: 'centered', - }, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Name: Story = { - render: () => { - return ; - }, -}; - -export const Complete: Story = { - render: () => { - return ; - }, -};