diff --git a/package.json b/package.json index 5d9a3eb..1085da1 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "prettier": "^3.2.5", + "sass": "^1.87.0", "typescript": "^5.2.2", "vite": "^5.0.8", "vite-plugin-radar": "^0.9.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77f54dd..f419dcd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,12 +100,15 @@ devDependencies: prettier: specifier: ^3.2.5 version: 3.2.5 + sass: + specifier: ^1.87.0 + version: 1.87.0 typescript: specifier: ^5.2.2 version: 5.3.3 vite: specifier: ^5.0.8 - version: 5.0.12(@types/node@20.12.12) + version: 5.0.12(@types/node@20.12.12)(sass@1.87.0) vite-plugin-radar: specifier: ^0.9.6 version: 0.9.6(vite@5.0.12) @@ -769,6 +772,149 @@ packages: fastq: 1.17.0 dev: true + /@parcel/watcher-android-arm64@2.5.1: + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-darwin-arm64@2.5.1: + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-darwin-x64@2.5.1: + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-freebsd-x64@2.5.1: + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm-glibc@2.5.1: + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm-musl@2.5.1: + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm64-glibc@2.5.1: + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-arm64-musl@2.5.1: + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-x64-glibc@2.5.1: + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-linux-x64-musl@2.5.1: + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-arm64@2.5.1: + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-ia32@2.5.1: + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher-win32-x64@2.5.1: + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@parcel/watcher@2.5.1: + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.5 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + dev: true + optional: true + /@remix-run/router@1.15.2: resolution: {integrity: sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==} engines: {node: '>=14.0.0'} @@ -1256,7 +1402,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.0 - vite: 5.0.12(@types/node@20.12.12) + vite: 5.0.12(@types/node@20.12.12)(sass@1.87.0) transitivePeerDependencies: - supports-color dev: true @@ -1402,6 +1548,13 @@ packages: supports-color: 7.2.0 dev: true + /chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + dependencies: + readdirp: 4.1.2 + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1505,6 +1658,14 @@ packages: engines: {node: '>=0.4.0'} dev: false + /detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + requiresBuild: true + dev: true + optional: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1899,6 +2060,10 @@ packages: engines: {node: '>= 4'} dev: true + /immutable@5.1.1: + resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==} + dev: true + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2154,6 +2319,12 @@ packages: tslib: 2.6.2 dev: true + /node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + requiresBuild: true + dev: true + optional: true + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -2347,6 +2518,11 @@ packages: loose-envify: 1.4.0 dev: false + /readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + dev: true + /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} dev: false @@ -2405,6 +2581,18 @@ packages: queue-microtask: 1.2.3 dev: true + /sass@1.87.0: + resolution: {integrity: sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 4.0.3 + immutable: 5.1.1 + source-map-js: 1.0.2 + optionalDependencies: + '@parcel/watcher': 2.5.1 + dev: true + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -2582,7 +2770,7 @@ packages: peerDependencies: vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 dependencies: - vite: 5.0.12(@types/node@20.12.12) + vite: 5.0.12(@types/node@20.12.12)(sass@1.87.0) dev: true /vite-plugin-svgr@4.2.0(typescript@5.3.3)(vite@5.0.12): @@ -2593,14 +2781,14 @@ packages: '@rollup/pluginutils': 5.1.0 '@svgr/core': 8.1.0(typescript@5.3.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - vite: 5.0.12(@types/node@20.12.12) + vite: 5.0.12(@types/node@20.12.12)(sass@1.87.0) transitivePeerDependencies: - rollup - supports-color - typescript dev: true - /vite@5.0.12(@types/node@20.12.12): + /vite@5.0.12(@types/node@20.12.12)(sass@1.87.0): resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -2632,6 +2820,7 @@ packages: esbuild: 0.19.12 postcss: 8.4.33 rollup: 4.9.6 + sass: 1.87.0 optionalDependencies: fsevents: 2.3.3 dev: true diff --git a/src/App.tsx b/src/App.tsx index 8659e32..1d42992 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,9 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { Routers } from "./routes"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import "./style/global.css"; import { Modal } from "@/components/common/modal/Modal.tsx"; +import "./style/global.css"; +import "./style/variable.scss"; const queryClient = new QueryClient(); diff --git a/src/api/index.ts b/src/api/index.ts index 129053d..7491c1e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -78,7 +78,8 @@ const onErrorResponse = (error: AxiosError | Error) => { if (axios.isAxiosError(error)) { const { message } = error; const { method, url } = error?.config as AxiosRequestConfig; - const { status, statusText } = error?.response as AxiosResponse; + const { status, statusText, data } = error?.response as AxiosResponse; + const description = data?.error?.message; logOnDev( `[API ERROR_RESPONSE ${status} | ${statusText} | ${message}] ${method?.toUpperCase()} ${url}`, @@ -86,26 +87,29 @@ const onErrorResponse = (error: AxiosError | Error) => { switch (status) { case 400: - onError(status, "잘못된 요청을 했어요"); + onError(status, description ?? "잘못된 요청을 했어요"); break; case 401: { - onError(status, "인증을 실패했어요"); + onError(status, description ?? "인증을 실패했어요"); break; } case 403: { - onError(status, "권한이 없는 상태로 접근했어요"); + onError(status, description ?? "권한이 없는 상태로 접근했어요"); break; } case 404: { - onError(status, "찾을 수 없는 페이지를 요청했어요"); + onError(status, description ?? "찾을 수 없는 페이지를 요청했어요"); break; } case 500: { - onError(status, "서버 오류가 발생했어요"); + onError(status, description ?? "서버 오류가 발생했어요"); break; } default: { - onError(status, `기타 에러가 발생했어요 : ${error?.message}`); + onError( + status, + description ?? `기타 에러가 발생했어요 : ${error?.message}`, + ); } } } else if (error instanceof Error && error?.name === "TimoutError") { diff --git a/src/app/home/index.tsx b/src/app/home/index.tsx index efb1387..ec019f5 100644 --- a/src/app/home/index.tsx +++ b/src/app/home/index.tsx @@ -195,7 +195,6 @@ export default function HomePage() { */ const openSubscriptionModal = () => { open({ - title: "구독 설정", content: , isVisibleBtn: false, }); diff --git a/src/app/signin/index.tsx b/src/app/signin/index.tsx index 979cc57..66578bc 100644 --- a/src/app/signin/index.tsx +++ b/src/app/signin/index.tsx @@ -85,7 +85,7 @@ export default function SignIn() { css={css` margin-top: 5.6rem; `} - state={isValidEmail && password.length > 5} + disabled={!(isValidEmail && password.length > 5)} onClick={handleSignIn} > 로그인 diff --git a/src/app/signup/consent/index.tsx b/src/app/signup/consent/index.tsx index c14b968..ed1d6d9 100644 --- a/src/app/signup/consent/index.tsx +++ b/src/app/signup/consent/index.tsx @@ -118,7 +118,7 @@ export default function Consent() { css={css` margin-top: 1.6rem; `} - state={privacy && useService} + disabled={!(privacy && useService)} onClick={() => handleNextClick()} > 다음 diff --git a/src/app/signup/index.tsx b/src/app/signup/index.tsx index d7361ed..1c2feda 100644 --- a/src/app/signup/index.tsx +++ b/src/app/signup/index.tsx @@ -79,9 +79,12 @@ export default function SignUp() { }} /> { - sendVerifyEmail({ email: email, emailPurpose: EMAIL_PURPOSE.SIGNUP}); + sendVerifyEmail({ + email: email, + emailPurpose: EMAIL_PURPOSE.SIGNUP, + }); setVerifySendCheck(true); }} > @@ -99,12 +102,12 @@ export default function SignUp() { maxLength={6} /> = 6 && !verifyEmailNumCheck} + disabled={verifyEmailNum.length >= 6 && !verifyEmailNumCheck} onClick={() => { verifyEmail({ email: email, randomNumber: verifyEmailNum, - emailPurpose: EMAIL_PURPOSE.SIGNUP + emailPurpose: EMAIL_PURPOSE.SIGNUP, }) .then(() => { setVerifyEmailNumCheck(true); @@ -163,7 +166,7 @@ export default function SignUp() { css={css` margin-top: 3.9rem; `} - state={Boolean( + disabled={Boolean( verifyEmailNumCheck && email && password && diff --git a/src/assets/checkbox/circle_checked_default.svg b/src/assets/checkbox/circle_checked_default.svg new file mode 100644 index 0000000..8a669b3 --- /dev/null +++ b/src/assets/checkbox/circle_checked_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/checkbox/circle_checked_fill.svg b/src/assets/checkbox/circle_checked_fill.svg new file mode 100644 index 0000000..f72f4e7 --- /dev/null +++ b/src/assets/checkbox/circle_checked_fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/chip/delete.svg b/src/assets/chip/delete.svg new file mode 100644 index 0000000..b33f8ea --- /dev/null +++ b/src/assets/chip/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/chip/plus.svg b/src/assets/chip/plus.svg new file mode 100644 index 0000000..bd77203 --- /dev/null +++ b/src/assets/chip/plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/app/signup/certificate-box.tsx b/src/components/app/signup/certificate-box.tsx index 039d788..84a7a67 100644 --- a/src/components/app/signup/certificate-box.tsx +++ b/src/components/app/signup/certificate-box.tsx @@ -1,17 +1,17 @@ import { css } from "@emotion/react"; import { PropsWithChildren } from "react"; -import { DESIGN_SYSTEM_COLOR, DESIGN_SYSTEM_TEXT } from "@/style/variable.ts"; +import { DESIGN_SYSTEM_TEXT } from "@/style/variable.ts"; interface certificateProps extends Omit, "type"> { icon?: string; - check?: boolean; + disabled?: boolean; } export default function CertificateBox({ icon, children, - check = false, + disabled = false, ...props }: PropsWithChildren) { return ( @@ -19,20 +19,26 @@ export default function CertificateBox({ id="certificateb-box" css={css` ${DESIGN_SYSTEM_TEXT.B2} - border-radius: 0.2rem; - padding: 0.5rem 1.1rem; - background: ${icon ? `transparent` : DESIGN_SYSTEM_COLOR.BRAND_BLUE}; - color: ${DESIGN_SYSTEM_COLOR.GRAY_50}; white-space: nowrap; width: auto; + border-radius: 6px; + padding: 11px 16px; + background: ${icon ? `transparent` : `var(--blue-5)`}; + color: var(--blue-50); transition: 0.4s all; + font-weight: 400; + font-size: 15px; cursor: pointer; + display: flex; + align-items: center; + ${icon && `padding-right: 0`} - ${!check && + ${disabled && css` pointer-events: none; - filter: grayscale(100%); + background-color: var(--gray-10); + color: var(--text-disabled); `} `} {...props} diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index f474c12..5449156 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -1,19 +1,16 @@ import { css } from "@emotion/react"; +// TODO: 버튼 컴포넌트 인라인 스타일 지정의 분리가 필요해요 + type buttonProps = { icon?: string; - state?: boolean; } & Omit, "types">; -export default function Button({ - icon, - state = true, - children, - ...props -}: buttonProps) { +export default function Button({ icon, children, ...props }: buttonProps) { return (
diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx index eadac76..327b519 100644 --- a/src/components/common/Input.tsx +++ b/src/components/common/Input.tsx @@ -4,7 +4,7 @@ import { DESIGN_SYSTEM_TEXT } from "@/style/variable.ts"; type inputProps = { placeholder: string; icon?: string; -} & Omit, "types">; +} & React.InputHTMLAttributes; export default function Input({ placeholder = "텍스트를 입력해주세요", icon, @@ -13,6 +13,7 @@ export default function Input({ return (
)} @@ -35,8 +36,10 @@ export default function Input({ border: none; box-shadow: inset 0 0 0 1px #eeeeee; width: 100%; - height: 5.6rem; - border-radius: 0.5rem; + height: 45px; + border-radius: 8px; + font-size: 14px; + color: var(--text-body1); padding-left: ${icon ? `6rem` : `2rem`}; &:focus { @@ -46,6 +49,11 @@ export default function Input({ &::placeholder { color: #bdbdbd; } + + &:disabled { + color: var(--text-disabled); + background: var(--gray-10); + } `} placeholder={placeholder} {...props} diff --git a/src/components/common/chip.tsx b/src/components/common/chip.tsx new file mode 100644 index 0000000..52dc711 --- /dev/null +++ b/src/components/common/chip.tsx @@ -0,0 +1,123 @@ +import { css } from "@emotion/react"; +import delete_icon from "@/assets/chip/delete.svg"; +import add_icon from "@/assets/chip/plus.svg"; +import { Fragment } from "react"; + +type ChipType = { + value?: string; + closable?: boolean; + inputable?: boolean; + onClick?: () => void; + onChange?: (e: React.ChangeEvent) => void; + onKeyUp?: (e: React.KeyboardEvent) => void; + onBlur?: (e: React.FocusEvent) => void; +} & React.InputHTMLAttributes; + +export default function Chip({ + value, + closable = false, + inputable = false, + onClick, + onChange, + onKeyUp, + onBlur, +}: ChipType) { + return ( + + {inputable ? ( +
+ 추가 아이콘 + +
+ ) : ( +
+ {value} + {closable ? ( + 삭제 아이콘 + ) : null} +
+ )} +
+ ); +} + +const inputableChipCss = css` + position: relative; + max-width: fit-content; + background-color: #fff; + border: 1px solid var(--gray-10); + border-radius: 4px; + padding: 3px 3px 3px 8px; + width: 100%; + height: 100%; + max-width: 86px; + max-height: fit-content; + box-sizing: border-box; + + display: flex; + align-items: center; + + & > input { + width: 100%; + height: 100%; + margin-left: 10px; + background-color: transparent; + border: none; + + &:focus { + outline: none; + } + + ::placeholder { + font-weight: 400; + } + } + + img { + z-index: 1; + position: absolute; + cursor: pointer; + } +`; + +const chipCss = css` + position: relative; + display: flex; + align-items: center; + column-gap: 2px; + background-color: #fff; + border: 1px solid var(--gray-10); + border-radius: 4px; + padding: 4px 8px; + width: 100%; + height: 100%; + max-width: fit-content; + max-height: fit-content; + + & > input { + width: fit-content; + background-color: transparent; + width: 100%; + max-width: 86px; + /* border: none; */ + + &:focus { + outline: none; + } + } + + & > img { + cursor: pointer; + } + + & > span { + color: var(--text-body2); + } +`; diff --git a/src/components/common/modal/Modal.tsx b/src/components/common/modal/Modal.tsx index 950a43f..695a21e 100644 --- a/src/components/common/modal/Modal.tsx +++ b/src/components/common/modal/Modal.tsx @@ -57,7 +57,7 @@ export function Modal() { background-color: #ffffff; border: 1px solid #cbcbcb; border-radius: 1rem; - padding: 2.9rem 2.4rem; + padding: 3.2rem; display: flex; flex-direction: column; text-align: center; @@ -68,7 +68,6 @@ export function Modal() { css={css` display: flex; flex-direction: column; - row-gap: 3rem; color: ${DESIGN_SYSTEM_COLOR.GRAY_700}; `} > @@ -92,7 +91,7 @@ export function Modal() {
{ - const [email, setEmail] = useState(""); - const [emailValid, setEmailValid] = useState(false); + const [verifyCheckBox, setVetifyCheckBox] = useState([ + { + id: "privacy", + checked: false, + label: "개인정보 처리방침", + url: "", + }, + { + id: "age", + checked: false, + label: "만 14세 이상입니다.", + url: "", + }, + ]); const [verifySendCheck, setVerifySendCheck] = useState(false); const [verifyEmailNum, setVerifyEmailNum] = useState(""); const [verifyEmailNumCheck, setVerifyEmailNumCheck] = useState(false); - const [categories, setCategories] = useState({ - bitcoinPrediction: false, - latestNews: false, + const [verifyDescription, setVerifyDescription] = useState<{ + type: "info" | "error" | "success"; + message: string; + }>({ + type: "info", + message: "", }); + const [remaingTime, setRemainingTime] = useState(300); + const [isTimerRunning, setIsTimerRunning] = useState(false); + const [timer, setTimer] = useState | null>( + null, + ); + const [keywordList, setKeywordList] = useState< + { value: string; inputable: boolean }[] + >([ + { + value: "", + inputable: true, + }, + ]); const { close } = useModal(); + const { email, isValidEmail, handleEmailChange } = useCheckEmail(); + + const handleAddkeyword = ({ + index, + keyword, + }: { + index: number; + keyword: { + value: string; + inputable: boolean; + }; + }) => { + if (keyword.inputable) { + if (!keyword.value.trim()) { + alert("올바른 키워드를 입력해주세요"); + return; + } + if (keywordList.length > 3) { + alert("최대 3개까지 입력 가능합니다."); + return; + } + } + Object.values(keywordList).forEach((item, i) => { + if (index !== i && item.value === keyword.value) { + alert("이미 입력된 키워드입니다."); + return; + } + }); + const newKeywordList = [...keywordList]; + if (newKeywordList[index].inputable) { + newKeywordList[index].inputable = false; + if (newKeywordList.length < 3) { + setKeywordList([...newKeywordList, { value: "", inputable: true }]); + } else { + setKeywordList(newKeywordList); + } + } else { + const updatedList = newKeywordList.filter((_, i) => i !== index); + if (updatedList.length === 0 || updatedList.every((k) => !k.inputable)) { + setKeywordList([...updatedList, { value: "", inputable: true }]); + } else { + setKeywordList(updatedList); + } + } + }; - const handleEmailChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setEmail(value); - setEmailValid(EMAIL_REGEX.test(value)); + const handleChangekeyword = ({ + index, + keyword, + }: { + index: number; + keyword: string; + }) => { + const newKeywordList = [...keywordList]; + newKeywordList[index].value = keyword; + setKeywordList(newKeywordList); }; const handleSendVerificationEmail = async () => { - if (!emailValid) return; + if (!isValidEmail) return; try { - sendVerifyEmail({ email, emailPurpose: EMAIL_PURPOSE.SUBSCRIBE }); - alert("인증번호가 전송되었습니다. 확인 후 입력해주세요."); - setVerifySendCheck(true); + const { status } = await sendVerifyEmail({ + email, + emailPurpose: EMAIL_PURPOSE.SUBSCRIBE, + }); + if (status === 200) { + handlePlayTimer(); + setVerifySendCheck(true); + } } catch (error) { - alert("이메일 전송에 실패했습니다. 다시 시도해주세요."); + setVerifyDescription({ + type: "error", + message: + (error as Error).message ?? + "이메일 전송에 실패했습니다. 다시 시도해주세요.", + }); } }; const handleVerifyEmailCode = async () => { if (!verifyEmailNum) return; + if (keywordList?.[0]?.value?.trim() === "") return; try { - await verifyEmail({ email, randomNumber: verifyEmailNum, emailPurpose: EMAIL_PURPOSE.SUBSCRIBE }); - setVerifyEmailNumCheck(true); - alert("이메일 인증이 완료되었습니다."); + const { status } = await verifyEmail({ + email, + randomNumber: verifyEmailNum, + emailPurpose: EMAIL_PURPOSE.SUBSCRIBE, + }); + if (status === 200) { + // 타이머 제거 및 중지 + setIsTimerRunning(false); + if (timer) clearInterval(timer); + // 인증번호 인증 완료 및 메세지 제공 + setVerifyEmailNumCheck(true); + setVerifyDescription({ + type: "success", + message: "인증되었습니다.", + }); + } } catch (error) { - alert("인증번호가 올바르지 않습니다. 다시 확인해주세요."); + setVerifyDescription({ + type: "error", + message: + (error as Error).message ?? + "인증 코드 인증이 실패했습니다. 다시 시도해주세요.", + }); } }; - const handleCompleteSubscription = async () => { - if (!verifyEmailNumCheck) { - alert("이메일 인증을 완료해주세요."); - return; + const handlePlayTimer = () => { + if (timer) { + clearInterval(timer); } - const selectedCategories = Object.keys(categories).filter( - (key) => categories[key as keyof typeof categories] - ); + setRemainingTime(300); + setIsTimerRunning(true); + setVerifyEmailNum(""); + setVerifyEmailNumCheck(false); + setVerifyDescription({ + type: "info", + message: "인증번호를 입력해주세요.", + }); - if (selectedCategories.length === 0) { - alert("최소 하나 이상의 구독 카테고리를 선택해주세요."); - return; - } + // 5분 타이머 시작, 돔에 나타내야함 + const newTimer = setInterval(() => { + setRemainingTime((prev) => { + if (prev <= 1) { + clearInterval(newTimer); + setIsTimerRunning(false); + setVerifySendCheck(false); + return 0; + } + return prev - 1; + }); + }, 1000); + + setTimer(newTimer); + }; + + const handleChecked = (item: { + id: string; + checked: boolean; + label: string; + url: string; + }) => { + const updatedCheckBox = verifyCheckBox.map((check) => { + if (check.id === item.id) { + return { ...check, checked: !check.checked }; + } + return check; + }); + setVetifyCheckBox(updatedCheckBox); + }; + + const handleCompleteSubscription = async () => { + const keywords = keywordList.reduce((acc, cur) => { + if (cur.value.trim() !== "") { + acc.push(cur.value); + } + return acc; + }, []); try { - await sendSubscribeEmail({ email }); + await sendSubscribeEmail({ email, keywords }); alert("구독이 완료되었습니다. 매일 자정에 구독 정보를 보내드립니다."); close(); // 모달 창 닫기 } catch (error) { - alert("구독 요청에 실패했습니다. 다시 시도해주세요."); + console.log(error); + setVerifyDescription({ + type: "error", + message: + (error as Error).message ?? + "구독 요청에 실패했습니다. 다시 시도해주세요.", + }); } }; - return (
-

- 구독할 카테고리를 선택하세요 -

- - {/* 구독 카테고리 토글 */} -
-