From f9d4fa4dbc69e113e295b8e2a55fe61046e3479d Mon Sep 17 00:00:00 2001 From: Kim Min-gyu <99083803+cobocho@users.noreply.github.com> Date: Fri, 14 Jun 2024 01:46:20 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=A8=EB=B3=B4=EB=94=A9=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 더미 사이드바 UI 구현 * feat: `Input` 컴포넌트 구현 * feat: `Checkbox` 구현 * feat: 회원가입 Form UI 구현 * feat: 로그인 Form 구현 * feat: 로그인 페이지 및 리다이렉트 구현 * docs: 이용 약관 작성 * feat: 회원가입 폼 UI 디자인 구현 * feat: 회원가입 서버 통신 로직 구현 * feat: 폼 제출간 로딩 구현 * feat: 온보딩 페이지 기초 레이아웃 구성 * feat: 온보딩 페이지 UI 구현 * feat: 온보딩 네트워크 통신 로직 구현 * feat: 유효성 검사 로직 일부 구현 * feat: 로그인 페이지 권한 검사 미들웨어 구현 * refactor: 미들웨어 추상화 * feat: 토큰 만료에 따른 refresh 로직 추가 * docs: 개인정보 처리방침 작성 * fix: 이용약관 및 개인정보 처리 방침 수정 * refactor: 메인 페이지 기본 딜레이 시간을 공용 함수에서 제거 * refactor: 권한 관리 미들웨어 리팩토링 * chore: 미사용 빌드 타임 스탬프 제거 * fix: 스토리북 패키지 의존성 수정 * fix: chromatic 워크플로우 수정 * chore: `SignUpForm` 스토리 제거 * feat: 스토리북에 msw 추가 * refactor: 테스트 환경 분리 * test: 유효성 검사 테스트 작성 * test: 퍼널 페이지 테스트 및 스토리 작성 * design: 스토리북 커스터마이징 * fix: 스토리북 데코레이터를 워크샵에서 관리하도록 수정 * fix: 에러 발생하는 스토리 빌드 제외 * fix: 스토리북 환경에서의 `prefetch` 비활성화 * refactor: `Storybook` 에러 해결을 위한 `Link` 래퍼 구현 related on: #22 * fix: 온보딩 뒤로 가기 제거 --- .github/workflows/chromatic.yml | 4 + .vscode/settings.json | 3 +- .../components/TermItem/TermItem.stories.tsx | 25 +- apps/extension/package.json | 1 + apps/web/.env.development | 3 + apps/web/.env.staging | 2 + apps/web/package.json | 20 +- apps/web/setupTests.ts | 29 + apps/web/src/app/(afterLogin)/layout.css.ts | 7 + apps/web/src/app/(afterLogin)/layout.tsx | 16 + apps/web/src/app/{ => (afterLogin)}/page.tsx | 6 +- .../src/app/(beforeLogin)/auth/token/page.tsx | 48 + .../src/app/(beforeLogin)/login/layout.css.ts | 10 + .../src/app/(beforeLogin)/login/layout.tsx | 9 + .../src/app/(beforeLogin)/login/page.css.ts | 5 + apps/web/src/app/(beforeLogin)/login/page.tsx | 15 + .../app/(beforeLogin)/signup/layout.css.ts | 10 + .../src/app/(beforeLogin)/signup/layout.tsx | 9 + .../src/app/(beforeLogin)/signup/loading.tsx | 5 + .../src/app/(beforeLogin)/signup/page.css.ts | 5 + .../web/src/app/(beforeLogin)/signup/page.tsx | 13 + .../src/app/(beforeLogin)/terms/layout.css.ts | 6 + .../src/app/(beforeLogin)/terms/layout.tsx | 9 + .../app/(beforeLogin)/terms/privacy/page.tsx | 386 ++++++ .../src/app/(beforeLogin)/terms/use/page.tsx | 483 +++++++ .../OnboardingHeader/OnboardingHeader.css.ts | 9 + .../OnboardingHeader/OnboardingHeader.tsx | 24 + .../_components/OnboardingHeader/index.ts | 1 + .../SelectBoxGroup/SelectBoxGroup.css.ts | 7 + .../SelectBoxGroup/SelectBoxGroup.tsx | 7 + .../_components/SelectBoxGroup/index.ts | 1 + .../_context/useOnboarding.spec.tsx | 61 + .../onboarding/_context/useOnboarding.tsx | 38 + .../onboarding/funnel/page.css.ts | 26 + .../onboarding/funnel/page.spec.tsx | 70 + .../onboarding/funnel/page.stories.tsx | 27 + .../(onboarding)/onboarding/funnel/page.tsx | 118 ++ .../(onboarding)/onboarding/job/page.css.ts | 26 + .../(onboarding)/onboarding/job/page.spec.tsx | 92 ++ .../onboarding/job/page.stories.tsx | 27 + .../app/(onboarding)/onboarding/job/page.tsx | 146 +++ .../app/(onboarding)/onboarding/layout.css.ts | 44 + .../app/(onboarding)/onboarding/layout.tsx | 36 + .../src/app/(onboarding)/onboarding/page.tsx | 7 + apps/web/src/app/layout.tsx | 12 +- apps/web/src/components/Link/Link.tsx | 8 + apps/web/src/components/Link/index.ts | 1 + .../src/components/LoginForm/LoginForm.css.ts | 20 + .../components/LoginForm/LoginForm.spec.tsx | 14 + .../LoginForm/LoginForm.stories.tsx | 25 + .../src/components/LoginForm/LoginForm.tsx | 31 + apps/web/src/components/LoginForm/index.ts | 1 + .../web/src/components/Sidebar/Sidebar.css.ts | 16 + .../components/Sidebar/Sidebar.stories.tsx | 14 + apps/web/src/components/Sidebar/Sidebar.tsx | 23 + apps/web/src/components/Sidebar/index.tsx | 1 + .../components/SignUpForm/SignUpForm.css.ts | 50 + .../components/SignUpForm/SignUpForm.spec.tsx | 140 ++ .../SignUpForm/SignUpForm.stories.tsx | 24 + .../src/components/SignUpForm/SignUpForm.tsx | 188 +++ apps/web/src/components/SignUpForm/index.ts | 1 + apps/web/src/components/Term/Term.tsx | 33 +- apps/web/src/components/index.tsx | 0 apps/web/src/middleware.ts | 131 ++ apps/web/src/styles/animations.css.ts | 12 + apps/web/src/styles/layout.ts | 1 + apps/web/src/utils/localStorage.ts | 20 + apps/web/src/utils/testing.tsx | 17 + apps/web/tsconfig.json | 7 +- .../web/vitest.config.ts | 11 +- apps/workshop/.storybook/main.ts | 14 +- apps/workshop/.storybook/manager.ts | 15 + apps/workshop/.storybook/preview.ts | 14 - apps/workshop/.storybook/preview.tsx | 44 + apps/workshop/.storybook/storybook.css | 4 + apps/workshop/package.json | 19 +- apps/workshop/public/Icons/backward.svg | 3 - apps/workshop/public/Icons/blog.svg | 8 - apps/workshop/public/Icons/close-circle.svg | 4 - apps/workshop/public/Icons/close.svg | 3 - apps/workshop/public/Icons/instagram.svg | 5 - apps/workshop/public/Icons/plus.svg | 3 - apps/workshop/public/Icons/search.svg | 3 - apps/workshop/public/Icons/symbol.svg | 5 - apps/workshop/public/Icons/typo.svg | 6 - apps/workshop/public/logo.png | Bin 0 -> 984 bytes apps/workshop/public/mockServiceWorker.js | 284 ++++ apps/workshop/tsconfig.json | 11 +- apps/workshop/vite.config.ts | 3 +- package.json | 18 +- packages/api/package.json | 8 +- packages/api/src/index.ts | 17 +- packages/api/src/lib/fetcher.ts | 99 +- packages/api/src/mocks/config.ts | 1 + packages/api/src/mocks/handlers/index.ts | 3 + packages/api/src/mocks/handlers/user.ts | 37 + .../services/useOnboardingMutation/model.ts | 28 + .../onboardingService.ts | 16 + .../services/useOnboardingMutation/queries.ts | 20 + .../services/useSearchQuery/searchService.ts | 1 - .../src/services/useSignUpMutation/model.ts | 9 + .../src/services/useSignUpMutation/queries.ts | 20 + .../useSignUpMutation/signUpService.ts | 16 + .../src/services/useUserInfoQuery/model.ts | 16 + .../src/services/useUserInfoQuery/queries.ts | 33 + .../useUserInfoQuery/userInfoService.ts | 15 + packages/api/src/shared/type.ts | 16 +- packages/design-system/package.json | 4 +- .../src/components/Button/Button.css.ts | 1 + .../src/components/Button/Button.spec.tsx | 8 +- .../src/components/Button/Button.tsx | 82 +- .../src/components/Checkbox/Checkbox.css.ts | 43 + .../src/components/Checkbox/Checkbox.spec.tsx | 30 + .../components/Checkbox/Checkbox.stories.tsx | 32 + .../src/components/Checkbox/Checkbox.tsx | 74 ++ .../src/components/Checkbox/index.ts | 1 + .../src/components/Icon/Icon.stories.tsx | 6 + .../src/components/Icon/icons/Spinner.tsx | 166 +++ .../src/components/Icon/icons/index.tsx | 3 + .../src/components/Input/Input.css.ts | 75 ++ .../src/components/Input/Input.spec.tsx | 97 ++ .../src/components/Input/Input.stories.tsx | 51 + .../src/components/Input/Input.tsx | 88 ++ .../src/components/Input/index.ts | 2 + .../src/components/SelectBox/SelectBox.css.ts | 64 + .../components/SelectBox/SelectBox.spec.tsx | 30 + .../SelectBox/SelectBox.stories.tsx | 40 + .../src/components/SelectBox/SelectBox.tsx | 61 + .../src/components/SelectBox/index.ts | 1 + .../src/components/Step/Step.css.ts | 26 + .../src/components/Step/Step.spec.tsx | 6 + .../src/components/Step/Step.stories.tsx | 29 + .../src/components/Step/Step.tsx | 24 + .../src/components/Step/index.ts | 1 + .../src/components/Text/Text.css.ts | 3 +- packages/design-system/src/index.ts | 8 +- packages/design-system/src/tokens/colors.ts | 11 + packages/design-system/vite.config.ts | 1 + pnpm-lock.yaml | 1155 +++++++++++++++-- turbo.json | 3 + 140 files changed, 5456 insertions(+), 288 deletions(-) create mode 100644 apps/web/.env.development create mode 100644 apps/web/setupTests.ts create mode 100644 apps/web/src/app/(afterLogin)/layout.css.ts create mode 100644 apps/web/src/app/(afterLogin)/layout.tsx rename apps/web/src/app/{ => (afterLogin)}/page.tsx (90%) create mode 100644 apps/web/src/app/(beforeLogin)/auth/token/page.tsx create mode 100644 apps/web/src/app/(beforeLogin)/login/layout.css.ts create mode 100644 apps/web/src/app/(beforeLogin)/login/layout.tsx create mode 100644 apps/web/src/app/(beforeLogin)/login/page.css.ts create mode 100644 apps/web/src/app/(beforeLogin)/login/page.tsx create mode 100644 apps/web/src/app/(beforeLogin)/signup/layout.css.ts create mode 100644 apps/web/src/app/(beforeLogin)/signup/layout.tsx create mode 100644 apps/web/src/app/(beforeLogin)/signup/loading.tsx create mode 100644 apps/web/src/app/(beforeLogin)/signup/page.css.ts create mode 100644 apps/web/src/app/(beforeLogin)/signup/page.tsx create mode 100644 apps/web/src/app/(beforeLogin)/terms/layout.css.ts create mode 100644 apps/web/src/app/(beforeLogin)/terms/layout.tsx create mode 100644 apps/web/src/app/(beforeLogin)/terms/privacy/page.tsx create mode 100644 apps/web/src/app/(beforeLogin)/terms/use/page.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.css.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/index.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.css.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/index.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.spec.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/funnel/page.css.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/funnel/page.spec.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/funnel/page.stories.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/funnel/page.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/job/page.css.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/job/page.spec.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/job/page.stories.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/job/page.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/layout.css.ts create mode 100644 apps/web/src/app/(onboarding)/onboarding/layout.tsx create mode 100644 apps/web/src/app/(onboarding)/onboarding/page.tsx create mode 100644 apps/web/src/components/Link/Link.tsx create mode 100644 apps/web/src/components/Link/index.ts create mode 100644 apps/web/src/components/LoginForm/LoginForm.css.ts create mode 100644 apps/web/src/components/LoginForm/LoginForm.spec.tsx create mode 100644 apps/web/src/components/LoginForm/LoginForm.stories.tsx create mode 100644 apps/web/src/components/LoginForm/LoginForm.tsx create mode 100644 apps/web/src/components/LoginForm/index.ts create mode 100644 apps/web/src/components/Sidebar/Sidebar.css.ts create mode 100644 apps/web/src/components/Sidebar/Sidebar.stories.tsx create mode 100644 apps/web/src/components/Sidebar/Sidebar.tsx create mode 100644 apps/web/src/components/Sidebar/index.tsx create mode 100644 apps/web/src/components/SignUpForm/SignUpForm.css.ts create mode 100644 apps/web/src/components/SignUpForm/SignUpForm.spec.tsx create mode 100644 apps/web/src/components/SignUpForm/SignUpForm.stories.tsx create mode 100644 apps/web/src/components/SignUpForm/SignUpForm.tsx create mode 100644 apps/web/src/components/SignUpForm/index.ts delete mode 100644 apps/web/src/components/index.tsx create mode 100644 apps/web/src/middleware.ts create mode 100644 apps/web/src/styles/animations.css.ts create mode 100644 apps/web/src/styles/layout.ts create mode 100644 apps/web/src/utils/localStorage.ts create mode 100644 apps/web/src/utils/testing.tsx rename vitest.config.mts => apps/web/vitest.config.ts (57%) create mode 100644 apps/workshop/.storybook/manager.ts delete mode 100644 apps/workshop/.storybook/preview.ts create mode 100644 apps/workshop/.storybook/preview.tsx delete mode 100644 apps/workshop/public/Icons/backward.svg delete mode 100644 apps/workshop/public/Icons/blog.svg delete mode 100644 apps/workshop/public/Icons/close-circle.svg delete mode 100644 apps/workshop/public/Icons/close.svg delete mode 100644 apps/workshop/public/Icons/instagram.svg delete mode 100644 apps/workshop/public/Icons/plus.svg delete mode 100644 apps/workshop/public/Icons/search.svg delete mode 100644 apps/workshop/public/Icons/symbol.svg delete mode 100644 apps/workshop/public/Icons/typo.svg create mode 100644 apps/workshop/public/logo.png create mode 100644 apps/workshop/public/mockServiceWorker.js create mode 100644 packages/api/src/mocks/config.ts create mode 100644 packages/api/src/mocks/handlers/index.ts create mode 100644 packages/api/src/mocks/handlers/user.ts create mode 100644 packages/api/src/services/useOnboardingMutation/model.ts create mode 100644 packages/api/src/services/useOnboardingMutation/onboardingService.ts create mode 100644 packages/api/src/services/useOnboardingMutation/queries.ts create mode 100644 packages/api/src/services/useSignUpMutation/model.ts create mode 100644 packages/api/src/services/useSignUpMutation/queries.ts create mode 100644 packages/api/src/services/useSignUpMutation/signUpService.ts create mode 100644 packages/api/src/services/useUserInfoQuery/model.ts create mode 100644 packages/api/src/services/useUserInfoQuery/queries.ts create mode 100644 packages/api/src/services/useUserInfoQuery/userInfoService.ts create mode 100644 packages/design-system/src/components/Checkbox/Checkbox.css.ts create mode 100644 packages/design-system/src/components/Checkbox/Checkbox.spec.tsx create mode 100644 packages/design-system/src/components/Checkbox/Checkbox.stories.tsx create mode 100644 packages/design-system/src/components/Checkbox/Checkbox.tsx create mode 100644 packages/design-system/src/components/Checkbox/index.ts create mode 100644 packages/design-system/src/components/Icon/icons/Spinner.tsx create mode 100644 packages/design-system/src/components/Input/Input.css.ts create mode 100644 packages/design-system/src/components/Input/Input.spec.tsx create mode 100644 packages/design-system/src/components/Input/Input.stories.tsx create mode 100644 packages/design-system/src/components/Input/Input.tsx create mode 100644 packages/design-system/src/components/Input/index.ts create mode 100644 packages/design-system/src/components/SelectBox/SelectBox.css.ts create mode 100644 packages/design-system/src/components/SelectBox/SelectBox.spec.tsx create mode 100644 packages/design-system/src/components/SelectBox/SelectBox.stories.tsx create mode 100644 packages/design-system/src/components/SelectBox/SelectBox.tsx create mode 100644 packages/design-system/src/components/SelectBox/index.ts create mode 100644 packages/design-system/src/components/Step/Step.css.ts create mode 100644 packages/design-system/src/components/Step/Step.spec.tsx create mode 100644 packages/design-system/src/components/Step/Step.stories.tsx create mode 100644 packages/design-system/src/components/Step/Step.tsx create mode 100644 packages/design-system/src/components/Step/index.ts diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 9148679..4b0b65a 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -45,6 +45,10 @@ jobs: run: pnpm install -no-frozen-lockfile working-directory: apps/workshop + - name: Build Design System Package + run: pnpm run build + working-directory: packages/design-system + - name: Publish Chromatic id: chromatic uses: chromaui/action@latest diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa621..2935aaf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "liveServer.settings.port": 5501 } diff --git a/apps/extension/components/TermItem/TermItem.stories.tsx b/apps/extension/components/TermItem/TermItem.stories.tsx index b24c03f..a6ac146 100644 --- a/apps/extension/components/TermItem/TermItem.stories.tsx +++ b/apps/extension/components/TermItem/TermItem.stories.tsx @@ -13,13 +13,13 @@ const meta = { '서버는 네트워크에 연결된 컴퓨터로 클라이언트의 요청을 받아 처리하고 결과를 응답으로 보냅니다. 웹 서버, 데이터 베이스 서버, 메일 서버 등이 서버에 속합니다.', }, }, - decorators: [ - (Story) => ( -
- -
- ), - ], + argTypes: { + term: { + control: { + type: 'object', + }, + }, + }, tags: ['autodocs'], } satisfies Meta @@ -27,4 +27,13 @@ export default meta type Story = StoryObj -export const Preview: Story = {} +export const Preview: Story = { + args: { + term: { + term: '서버', + synonyms: 'Server', + meaning: + '서버는 네트워크에 연결된 컴퓨터로 클라이언트의 요청을 받아 처리하고 결과를 응답으로 보냅니다. 웹 서버, 데이터 베이스 서버, 메일 서버 등이 서버에 속합니다.', + }, + }, +} diff --git a/apps/extension/package.json b/apps/extension/package.json index 48b2e45..4409c3d 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -34,6 +34,7 @@ "@types/node": "^20.11.24", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.3.1", "@vitest/ui": "^1.6.0", "@vook-client/eslint-config": "*", "eslint": "^8.57.0", diff --git a/apps/web/.env.development b/apps/web/.env.development new file mode 100644 index 0000000..b59839d --- /dev/null +++ b/apps/web/.env.development @@ -0,0 +1,3 @@ +NEXT_PUBLIC_DOMAIN=http://localhost:3000 +NEXT_PUBLIC_API_URL=https://dev.vook-api.seungyeop-lee.com +NEXT_PUBLIC_GOOGLE_LOGIN_URL=https://dev.vook-api.seungyeop-lee.com/oauth2/authorization/google diff --git a/apps/web/.env.staging b/apps/web/.env.staging index 8f97cda..d8d4497 100644 --- a/apps/web/.env.staging +++ b/apps/web/.env.staging @@ -1 +1,3 @@ +NEXT_PUBLIC_DOMAIN=https://stag.vook.seungyeop-lee.com NEXT_PUBLIC_API_URL=https://stag.vook-api.seungyeop-lee.com +NEXT_PUBLIC_GOOGLE_LOGIN_URL=https://stag.vook-api.seungyeop-lee.com/oauth2/authorization/google diff --git a/apps/web/package.json b/apps/web/package.json index 0e7ec4a..8bb278d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,10 +6,13 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint . --max-warnings 0" + "lint": "eslint . --max-warnings 0", + "test": "vitest" }, "dependencies": { + "@hookform/resolvers": "^3.6.0", "@tanstack/react-query": "^5.32.0", + "@types/js-cookie": "^3.0.6", "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/css-utils": "^0.1.3", "@vanilla-extract/dynamic": "^2.1.0", @@ -17,9 +20,13 @@ "@vanilla-extract/sprinkles": "^1.6.1", "@vook-client/api": "*", "@vook-client/design-system": "*", + "js-cookie": "^3.0.5", "next": "^14.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.51.5", + "vite-tsconfig-paths": "^4.3.2", + "zod": "^3.23.8", "zustand": "^4.5.2" }, "devDependencies": { @@ -27,15 +34,24 @@ "@storybook/react": "^8.0.10", "@storybook/test": "^8.0.10", "@tanstack/react-query-devtools": "^5.32.0", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^15.0.5", + "@testing-library/user-event": "^14.5.2", "@types/eslint": "^8.56.5", "@types/node": "^20.11.24", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", "@vanilla-extract/next-plugin": "^2.4.0", + "@vanilla-extract/vite-plugin": "^4.0.9", "@vanilla-extract/webpack-plugin": "^2.3.7", + "@vitejs/plugin-react": "^4.3.1", "@vook-client/eslint-config": "*", "@vook-client/typescript-config": "*", "clsx": "^2.1.1", - "eslint": "^8.57.0" + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "jsdom": "^24.0.0", + "msw": "^2.3.1", + "vitest": "^1.5.2" } } diff --git a/apps/web/setupTests.ts b/apps/web/setupTests.ts new file mode 100644 index 0000000..99dbf39 --- /dev/null +++ b/apps/web/setupTests.ts @@ -0,0 +1,29 @@ +import '@testing-library/jest-dom' +import { handlers } from '@vook-client/api' +import { setupServer } from 'msw/node' +import { cleanup } from '@testing-library/react' +import { afterEach } from 'vitest' + +const mswServer = setupServer(...handlers) + +vi.mock('next/font/local', () => { + return { + default: () => {}, + } +}) + +vi.mock('next/navigation', () => ({ + useRouter() { + return { + prefetch: () => null, + } + }, +})) + +afterEach(() => { + cleanup() +}) + +beforeAll(() => mswServer.listen()) + +afterAll(() => mswServer.close()) diff --git a/apps/web/src/app/(afterLogin)/layout.css.ts b/apps/web/src/app/(afterLogin)/layout.css.ts new file mode 100644 index 0000000..276cce0 --- /dev/null +++ b/apps/web/src/app/(afterLogin)/layout.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css' + +import { SIDE_BAR_WIDTH } from '@/styles/layout' + +export const mainArea = style({ + marginLeft: SIDE_BAR_WIDTH, +}) diff --git a/apps/web/src/app/(afterLogin)/layout.tsx b/apps/web/src/app/(afterLogin)/layout.tsx new file mode 100644 index 0000000..9558ca4 --- /dev/null +++ b/apps/web/src/app/(afterLogin)/layout.tsx @@ -0,0 +1,16 @@ +import React, { PropsWithChildren } from 'react' + +import { Sidebar } from '@/components/Sidebar' + +import { mainArea } from './layout.css' + +const Layout = ({ children }: PropsWithChildren) => { + return ( +
+ + {children} +
+ ) +} + +export default Layout diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/(afterLogin)/page.tsx similarity index 90% rename from apps/web/src/app/page.tsx rename to apps/web/src/app/(afterLogin)/page.tsx index ae8db1a..0fe9649 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/(afterLogin)/page.tsx @@ -5,16 +5,14 @@ import { Term } from '@/components/Term/Term' const Home = () => { return ( -
+
-
-
-
+ ) } diff --git a/apps/web/src/app/(beforeLogin)/auth/token/page.tsx b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx new file mode 100644 index 0000000..dc8aee8 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx @@ -0,0 +1,48 @@ +'use client' + +import { userInfoService, UserStatus } from '@vook-client/api' +import Cookies from 'js-cookie' +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' + +interface AuthCallbackQueryParams { + searchParams: { + access: string + refresh: string + } +} + +const AuthCallbackPage = ({ + searchParams: { access, refresh }, +}: AuthCallbackQueryParams) => { + const router = useRouter() + + useEffect(() => { + Cookies.set('access', access, { + secure: true, + }) + Cookies.set('refresh', refresh, { + secure: true, + }) + + const checkUserRegistered = async () => { + const userInfo = await userInfoService.getUserInfo({ + access, + refresh, + }) + const isRegistered = userInfo.result.status === UserStatus.Registered + + if (isRegistered) { + router.push('/') + return + } + router.push('/signup') + } + + checkUserRegistered() + }, [access, refresh, router]) + + return null +} + +export default AuthCallbackPage diff --git a/apps/web/src/app/(beforeLogin)/login/layout.css.ts b/apps/web/src/app/(beforeLogin)/login/layout.css.ts new file mode 100644 index 0000000..2e7016d --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/login/layout.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css' + +export const loginLayout = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + width: '100dvw', + height: '100dvh', +}) diff --git a/apps/web/src/app/(beforeLogin)/login/layout.tsx b/apps/web/src/app/(beforeLogin)/login/layout.tsx new file mode 100644 index 0000000..3628692 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/login/layout.tsx @@ -0,0 +1,9 @@ +import React, { PropsWithChildren } from 'react' + +import { loginLayout } from './layout.css' + +const Layout = ({ children }: PropsWithChildren) => { + return
{children}
+} + +export default Layout diff --git a/apps/web/src/app/(beforeLogin)/login/page.css.ts b/apps/web/src/app/(beforeLogin)/login/page.css.ts new file mode 100644 index 0000000..84f7501 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/login/page.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css' + +export const loginFormArea = style({ + width: 380, +}) diff --git a/apps/web/src/app/(beforeLogin)/login/page.tsx b/apps/web/src/app/(beforeLogin)/login/page.tsx new file mode 100644 index 0000000..9796f42 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/login/page.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +import { LoginForm } from '@/components/LoginForm' + +import { loginFormArea } from './page.css' + +const LoginPage = () => { + return ( +
+ +
+ ) +} + +export default LoginPage diff --git a/apps/web/src/app/(beforeLogin)/signup/layout.css.ts b/apps/web/src/app/(beforeLogin)/signup/layout.css.ts new file mode 100644 index 0000000..2e7016d --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/signup/layout.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css' + +export const loginLayout = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + width: '100dvw', + height: '100dvh', +}) diff --git a/apps/web/src/app/(beforeLogin)/signup/layout.tsx b/apps/web/src/app/(beforeLogin)/signup/layout.tsx new file mode 100644 index 0000000..cc7cf2f --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/signup/layout.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react' + +import { loginLayout } from './layout.css' + +const Layout = ({ children }: PropsWithChildren) => { + return
{children}
+} + +export default Layout diff --git a/apps/web/src/app/(beforeLogin)/signup/loading.tsx b/apps/web/src/app/(beforeLogin)/signup/loading.tsx new file mode 100644 index 0000000..dbfccb0 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/signup/loading.tsx @@ -0,0 +1,5 @@ +const Loading = () => { + return
+} + +export default Loading diff --git a/apps/web/src/app/(beforeLogin)/signup/page.css.ts b/apps/web/src/app/(beforeLogin)/signup/page.css.ts new file mode 100644 index 0000000..e5f864b --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/signup/page.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css' + +export const signupForm = style({ + width: 380, +}) diff --git a/apps/web/src/app/(beforeLogin)/signup/page.tsx b/apps/web/src/app/(beforeLogin)/signup/page.tsx new file mode 100644 index 0000000..27bb455 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/signup/page.tsx @@ -0,0 +1,13 @@ +import { SignUpForm } from '@/components/SignUpForm' + +import { signupForm } from './page.css' + +const SignUpPage = () => { + return ( +
+ +
+ ) +} + +export default SignUpPage diff --git a/apps/web/src/app/(beforeLogin)/terms/layout.css.ts b/apps/web/src/app/(beforeLogin)/terms/layout.css.ts new file mode 100644 index 0000000..9a434b5 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/terms/layout.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css' + +export const termsLayout = style({ + width: 780, + margin: '30px auto', +}) diff --git a/apps/web/src/app/(beforeLogin)/terms/layout.tsx b/apps/web/src/app/(beforeLogin)/terms/layout.tsx new file mode 100644 index 0000000..1d4e731 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/terms/layout.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react' + +import { termsLayout } from './layout.css' + +const Layout = ({ children }: PropsWithChildren) => { + return
{children}
+} + +export default Layout diff --git a/apps/web/src/app/(beforeLogin)/terms/privacy/page.tsx b/apps/web/src/app/(beforeLogin)/terms/privacy/page.tsx new file mode 100644 index 0000000..0e411bd --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/terms/privacy/page.tsx @@ -0,0 +1,386 @@ +import { Text } from '@vook-client/design-system' +import React from 'react' + +const PrivacyTermPage = () => { + return ( +
+
+ + 개인정보 처리방침 + +
+
+
+ + 부크 개인정보 처리방침 + +
+
+
+ + 용사팀(이하‘회사’)은 개인정보 보호법 제30조에 따라 정보주체의 개인정보 + 보호 및 권익을 보호하고 개인정보와 관련한 정보주체의 고충을 원활하게 + 처리할 수 있도록 다음과 같이 개인정보 처리 방침을 수립 공개합니다. + +
+
+
+ + 제1조(수집하는 개인정보 항목 및 수집 방법) + +
+ + 회사는 회원가입, 원활한 고객상담, 각종 서비스 등 기본적인 서비스 + 제공을 위한 개인정보를 수집할 수 있습니다. 이용자의 기본적 인권 침해의 + 우려가 있는 민감한 개인정보(인종, 사상 및 신조, 정치적 성향이나 + 범죄기록, 의료 정보 등)는 기본적으로 수집하지 않습니다. 단, 불가피하게 + 수집이 필요한 경우 반드시 사전에 동의 절차를 거치도록 하겠습니다. + +
+ + ① 수집항목 : 닉네임, 이메일 주소 + +
+ + ② 개인정보 수집 방법 : 웹 회원가입, 회원정보 수정 + +
+ + 인터넷 서비스 이용과정에서 아래 개인정보 항목이 자동으로 생성되어 + 수집될 수 있습니다. • IP 주소, 쿠키, 서비스 이용 기록, 방문 기록, 불량 + 이용 기록 등 + +
+ + 제2조(개인정보의 수집 및 이용목적) + +
+ + 회사는 수집한 개인정보를 다음의 목적을 위해 활용합니다. 이용자가 + 제공한 모든 정보는 하기 목적에 필요한 용도 이외로는 사용되지 않으며 + 이용 목적이 변경될 시에는 사전 동의를 구할 것입니다. + +
+ + ① 회원 관리 : 회원제 서비스 이용에 따른 본인확인, 개인 식별, 불량 + 회원의 부정 이용 방지와 비인가 사용 방지, 가입 의사 확인, 불만 처리 등 + 민원처리, 고지사항 전달 + +
+ + ② 마케팅 및 광고에 활용 : 신규 서비스 개발과 이벤트 행사에 따른 정보 + 전달 및 맞춤 서비스 제공, 인구통계학적 특성에 따른 서비스 제공, 접속 + 빈도 파악 또는 회원의 서비스 이용에 대한 통계 + +
+ + 제3조(개인정보의 보유 및 이용기간) + +
+ + 회사는 정보주체로부터 개인정보를 수집할 때 동의 받은 개인정보 보유 및 + 이용기간 또는 법령에 따른 개인정보 보유 및 이용기간 내에서 개인정보를 + 처리․보유합니다. + +
+ + 원칙적으로, 개인정보 수집 및 이용목적이 달성된 후에는 해당 정보를 지체 + 없이 파기합니다. 단, 관계법령의 규정에 의하여 보존할 필요가 있는 경우 + 회사는 아래와 같이 관계법령에서 정한 일정한 기간 동안 회원정보를 + 보관합니다. + +
+ + ① 표시/광고에 관한 기록 : 6개월 (전자상거래등에서의 소비자보호에 관한 + 법률) + +
+ + ② 계약 또는 청약철회 등에 관한 기록 : 5년 (전자상거래등에서의 + 소비자보호에 관한 법률) + +
+ + ③ 대금결제 및 재화 등의 공급에 관한 기록 : 5년 (전자상거래등에서의 + 소비자보호에 관한 법률) + +
+ + ④ 소비자의 불만 또는 분쟁처리에 관한 기록 : 3년 (전자상거래등에서의 + 소비자보호에 관한 법률) + +
+ + ⑤ 신용정보의 수집/처리 및 이용 등에 관한 기록 : 3년 (신용정보의 이용 + 및 보호에 관한 법률) + +
+ + 제4조(개인정보의 제3자 제공) + +
+ + 회사는 정보주체의 개인정보를 제1조(개인정보의 처리 목적)에서 명시한 + 범위 내에서만 처리하며, 정보주체의 동의, 법률의 특별한 규정 등 + 개인정보 보호법 제17조에 해당하는 경우에만 개인정보를 제3자에게 + 제공합니다. + +
+ + 제5조(개인정보처리 위탁) + +
+ + 회사는 경영 효율성의 제고, 서비스 품질 향상 등을 위하여 업무의 일부를 + 외부 전문업체 등 제 3자에게 용역을 주어 수행하며, 이를 위해 제3자에게 + 매니저의 개인정보 및 사업자가 보유하고 있는 고객 정보를 수집, 보관, + 처리, 이용, 제공, 관리, 파기 등을 할 수 있도록 업무를 처리위탁합니다. + 이와 관련하여, 수탁자 및 위탁업무의 내용은 다음과 같습니다. + +
+ + 수탁업체 : Oracle Cloud Infrastructure(OCI) + +
+ + 위탁업무 : 서비스 제공을 위한 서버 운영 + +
+ + 개인정보 보유 및 이용기간 : 회원 탈퇴 시, 서비스 종료 시 혹은 위탁 + 계약 종료 시까지 + +
+ + 회사는 개인정보를 국외의 다른 사업자에게 제공하지 않습니다. 다만, + 정보통신서비스의 제공에 관한 계약 이행 및 이용자 편의 증진 등을 위하여 + 다음과 같이 개인정보 처리 업무를 국외에 위탁하고 있습니다. + +
+ + 수탁업체 : Goolge + +
+ + 이전목적 : 사용자 데이터 관리 + +
+ + 이전되는 국가 : 미국 + +
+ + 이전일시 및 방법 : 서비스 이용 시마다 네트워크를 통해 이전 + +
+ + 개인정보 보유 및 이용기간 : 회원 탈퇴 시, 서비스 종료 시 혹은 위탁 + 계약 종료 시까지 + +
+ + 회사는 위탁계약 체결시 개인정보 보호법 제25조에 따라 위탁업무 수행목적 + 외 개인정보 처리금지, 기술적․관리적 보호조치, 재위탁 제한, 수탁자에 + 대한 관리․감독, 손해배상 등 책임에 관한 사항을 계약서 등 문서에 + 명시하고, 수탁자가 개인정보를 안전하게 처리하는지를 감독하고 있습니다. + +
+ + 위탁업무의 내용이나 수탁자가 변경될 경우에는 지체없이 본 개인정보 + 처리방침을 통하여 공개하도록 하겠습니다. + +
+ + 제6조(개인정보 자동수집장치의 설치, 운영에 대한 사항) + +
+ + 회사는 제공하는 서비스를 통해 이용자의 정보를 저장하고 수시로 찾아내는 + 쿠키(cookie)를 설치하고 운용할 수도 있습니다. 회사가 쿠키(cookie)를 + 통해 수집한 고객의 정보는 다음의 목적을 위해 사용될 수 있습니다. + +
+ + ① 용어 작성/검색 시 회원별 차별화된 정보를 제공 + +
+ + ② 회원과 비회원의 접속빈도 또는 머문 시간 등을 분석하여 이용자의 + 취향과 관심분야를 파악하여 특화된 서비스 제공 + +
+ + ③ 이용자들의 습관을 분석하여 서비스 개편 등의 척도로 활용 + +
+ + 이용자는 쿠키 설치에 대해 거부할 수 있습니다. 단, 쿠키 설치를 + 거부하였을 경우 서비스를 정상적으로 이용하지 못 할 수 있습니다. 쿠키 + 설치 거부 방법은 다음과 같습니다. (인터넷 익스플로러 기준) 웹 브라우저 + 상단의 도구 {'>'} 인터넷 옵션 {'>'} 개인정보 {'>'} 사이트 차단 + +
+ + 제7조(개인정보의 파기절차 및 방법) + +
+ + 회사는 수집 및 이용목적이 달성되면 매니저의 개인정보를 지체없이 + 파기하며, 절차 및 방법은 아래와 같습니다. + +
+ + ① 전자적 파일 형태인 경우 복구 및 재생되지 않도록 안전하게 삭제하고, + 그 밖에 기록물, 인쇄물, 서면 등의 경우 분쇄하거나 소각하여 파기합니다. + +
+ + ② 파기절차이용자가 입력한 정보는 목적 달성 후 별도의 DB에 + 옮겨져(종이의 경우 별도의 서류) 내부 방침 및 기타 관련 법령에 따라 + 일정기간 저장된 후 혹은 즉시 파기됩니다. 이 때, DB로 옮겨진 개인정보는 + 법률에 의한 경우가 아니고서는 다른 목적으로 이용되지 않습니다. + +
+ + ③ 파기기한이용자의 개인정보는 개인정보의 보유기간이 경과된 경우에는 + 보유기간의 종료일로부터 5일 이내에, 개인정보의 처리 목적 달성, 해당 + 서비스의 폐지, 사업의 종료 등 그 개인정보가 불필요하게 되었을 때에는 + 개인정보의 처리가 불필요한 것으로 인정되는 날로부터 5일 이내에 그 + 개인정보를 파기합니다. + +
+ + 회사는 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 전자금융거래법 + 및 기타 관계법령에 따른 개인정보 보관 규칙을 성실하게 수행하고 + 있습니다. + +
+ + 제8조(개인정보의 안전성 확보) + +
+ + 회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 + 있습니다. + +
+ + ① 개인정보 보호책임자의 지정 등 개인정보 보호 조직의 구성 및 운영에 + 관한 사항 등을 포함하여 개인정보 내부관리 계획을 수립하고, 매년 + 내부관리 계획을 잘 시행하고 있는지를 점검하고 있습니다. + +
+ + ② 개인정보를 처리하는 데이터베이스 시스템에 대한 접근 권한 관리를 통해 + 개인정보에 대한 접근을 통제하고 내∙외부로부터의 무단 접근을 통제하고 + 있습니다. + +
+ + ③ 개인정보 취급자가 개인정보처리시스템에 접속한 기록을 보관 및 + 관리하며, 개인정보의 오남용, 분실, 위조 및 변조 등을 방지하기 위하여 + 접속기록 등을 정기적으로 점검하며, 개인정보 취급자의 접속기록이 위, + 변조 및 도난, 분실되지 않도록 해당 접속기록을 안전하게 보관하고 + 있습니다. + +
+ + ④ 개인정보를 보관하고 있는 물리적 보관 장소를 별도로 두고 이에 대해 + 출입통제 절차를 수립, 운영하고 있습니다. + +
+ + 제9조(개인정보 보호책임자) + +
+ + 회사는 이용자의 개인정보를 보호하고 개인정보와 관련한 불만을 처리하기 + 위하여 아래와 같이 개인정보관리 책임자를 지정하고 있습니다. 이용자는 + 회사의 서비스를 이용하시며 발생하는 모든 개인정보보호 관련 민원을 + 개인정보관리 책임자로 신고하실 수 있습니다. 회사는 이용자들의 + 신고사항에 대해 신속하게 충분한 답변을 드릴 것입니다. + +
+ + 개인정보 관리책임자 • 성명 : 박재은 • 연락처 : pitapan1248@gmail.com + +
+ + 제10조(개인정보 처리방침 변경) + +
+ + ① 회사는 이 개인정보 처리방침의 내용과 상호 및 대표자 성명, 영업소 + 소재지 주소, 전자우편주소, 사업자등록번호, 통신판매업 신고번호, + 개인정보관리책임자등을 연결화면을 통하여 게시합니다. + +
+ + ② 회사는 회원가입시 이용자에게 회원가입 행위가 개인정보 처리방침에 + 동의하는 행위임을 알리고 개인정보 처리방침의 내용을 연결화면을 통하여 + 볼 수 있도록 합니다. + +
+ + ③ 회사는 관련 법을 위배하지 않는 범위에서 이 개인정보 처리방침을 + 개정할 수 있습니다. + +
+ + ④ 회사가 개인정보 처리방침을 개정할 경우에는 개정사유를 명시하여 현행 + 개인정보 처리방침, 개정 개인정보 처리방침을 모두 볼 수 있도록 하여 + 서비스의 게시판에 개정 개인정보 처리방침 적용 7일 이전 부터 적용일자 + 전일 까지 공지하며 회원 각 개인의 이메일로 개정 개인정보 처리방침 적용 + 7일 이전에 개정 내용을 발송합니다. + +
+ + ⑤ 회사가 개인정보처리방침은 시행일로부터 적용되며, 법령, 정부의 정책 + 또는 회사 내 부정책 등에 따른 변경 내용의 추가, 삭제 및 정정이 있는 + 경우에는 변경 사항의 시행 7일 전부터 공지사항을 통하여 고지할 + 것입니다. 개인정보 처리방침 적용 7일 이전 부터 적용일자 전일 까지 이의 + 의사를 회사측에 표명하지 않으면 개정 개인정보 처리방침에 동의하는 + 것으로 간주합니다. + +
+ + 제11조(권익침해 구제방법) + +
+ + 개인정보주체는 개인정보침해로 인한 구제를 받기 위하여 + 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 + 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 + 신고 및 상담에 대하여는 아래의 기관에 문의하시기를 바랍니다. + +
+ + ① 개인분쟁조정위원회 : (국번없이)118 + +
+ + ② 정보보호마크인증위원회 : 02-580-0533~4 ( + http://eprivacy.or.kr) + +
+ + ③ 대검찰청 사이버범죄수사단 : 02-3480-3573 ( + http://www.spo.go.kr/) + +
+ + ④ 경찰청 사이버테러대응센터 : 02-1566-0112 ( + http://www.netan.go.kr/) + +
+ + 부칙 이 약관은 2024. 06. 07부터 시행합니다. + + +
+
+
+ ) +} + +export default PrivacyTermPage diff --git a/apps/web/src/app/(beforeLogin)/terms/use/page.tsx b/apps/web/src/app/(beforeLogin)/terms/use/page.tsx new file mode 100644 index 0000000..49308c8 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/terms/use/page.tsx @@ -0,0 +1,483 @@ +import { Text } from '@vook-client/design-system' +import React from 'react' + +const TermsOfUsePage = () => { + return ( +
+
+ + 이용약관 + +
+
+ + Vook 약관 + +
+
+
+
+ + 제 1조(목적) + +
+
+ + 본 약관은 (이하 “회사”라 합니다)이 부크 웹페이지를 통해(이하 “서비스 + 페이지”라 합니다) 제공하는 부크 및 부크 관련 제반 서비스(이하 + “서비스”라 합니다)의 이용과 관련하여, 회사와 회원과의 권리, 의무 및 + 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다. + +
+
+ + 제 2조(용어의 정의) + +
+
+ + ① "서비스"란 회사가 서비스 페이지를 통해 제공하는 용어집 관리 관련 + 모든 서비스를 통칭하며, 아울러 서비스를 운영하는 회사의 의미로도 + 사용합니다. + +
+ + ② "이용자"란 서비스에 접속하여 이 약관에 따라 서비스를 받는 회원 및 + 비회원을 말합니다. + +
+ + ③ "회원"이라 함은 서비스에 개인정보를 제공하여 등록을 한 자로서, + 서비스의 정보를 지속적으로 제공받으며, 서비스를 계속적으로 이용할 수 + 있는 자를 말합니다. + +
+ + ⑤ "비회원"이라 함은 회원에 가입하지 않고 회사가 제공하는 서비스를 + 이용하는 자를 말합니다. + +
+ + ⑥ 이 약관에서 사용하는 용어의 정의는 제1항, 제2항, 제3항, 제4항에서 + 정하는 것을 제외하고는 관련법령에서 정하는 바에 의하며, 관련 법령에서 + 정하지 않는 것은 일반적인 상관례에 의합니다. + +
+
+ + 제 3조(약관의 명시와 개정) + +
+
+ + ① 회사는 본 약관의 내용과 상호, 이메일 등을 회원이 쉽게 알 수 있도록 + 서비스 초기 화면에 게시하거나 기타의 방법으로 회원에게 공지합니다. + +
+ + ② 회사는 「약관의 규제에 관한 법률」, 「전기통신기본법」, + 「전기통신사업법」, 「정보통신망 이용 촉진 및 정보보호 등에 관한 + 법률」, 「개인정보 보호법」 등 관련 법령을 위배하지 않는 범위에서 이 + 약관을 개정할 수 있습니다. + +
+ + ③ 회사가 약관을 개정할 경우에는 적용일자 및 개정 사유를 명시하여 현행 + 약관과 함께 개정약관의 적용일자 7일 전부터 적용일자 전일까지 서비스 + 페이지에 공지합니다. 다만, 회원의 권리, 의무에 중대한 영향을 주는 + 변경의 경우에는 적용일자 30일 전까지 공지합니다. + +
+ + ④ 회원은 개정 약관에 대하여 거부할 권리가 있습니다. 다만, 회사가 + 제3항에 따라 공지한 개정 약관의 적용/시행일까지 회원이 거부 의사를 + 표시하지 아니할 경우 회사는 회원이 적용/시행일자부로 변경 약관에 + 동의한 것으로 봅니다. 개정/변경 약관에 대하여 거부 의사를 표시한 + 회원은 계약의 해지 또는 회원 탈퇴를 선택할 수 있습니다. + +
+
+ + 제 4조(약관 외 준칙) + +
+
+ + 본 약관에 규정되지 않은 사항에 대해서는 관련법령 또는 회사가 정한 개별 + 서비스의 이용약관, 운영정책 및 규칙 등(이하 ‘세부지침’)의 규정에 + 따릅니다. + +
+
+ + 제 5조(이용계약의 성립) + +
+
+ + ① 회사와 회원 사이의 서비스 이용계약(이하 “이용계약”이라 합니다)은 + 서비스를 이용하고자 하는 자(이하 “가입신청자”라 합니다)가 회원 가입에 + 필요한 정보를 기입 후 이용약관과 개인정보 수집 및 이용 등에 명시적인 + 동의 의사표시를 하고, 회사가 이에 대하여 이용 승낙을 함으로써 + 성립합니다. + +
+ + ② 회사는 가입신청자의 신청에 대하여 서비스 이용을 승낙함을 원칙으로 + 합니다. + +
+ + ③ 회사는 서비스 관련설비의 여유가 없거나, 기술상 또는 업무상 문제가 + 있는 경우에는 승낙을 유보할 수 있습니다. + +
+ + ④ 회사가 제2항, 제3항에 따라 회원가입신청을 승낙하지 아니하거나 + 유보하는 경우, 회사는 원칙적으로 그 사실을 해당 사유와 함께 + 가입신청자가 기입한 이메일 주소로 통지합니다. 단 회사는 이러한 승낙의 + 거부 또는 유보에 대한 사실을 통지하는 행위가 법령에 위반되거나 기타 + 회사의 본 서비스 제공에 중대하게 부정적인 영향을 미칠 경우 이러한 + 통지를 생략할 수 있습니다. + +
+ + ⑤ 이용계약의 성립 시기는 회사가 가입 완료를 신청 절차 상에서 표시한 + 시점으로 합니다. 회사는 회원에 대해 회사 정책에 따라 등급별로 구분하여 + 제공하는 서비스 기능 등을 세분하여 이용에 차등을 둘 수 있습니다. + +
+ + ⑥ 이 약관은 회원이 이 약관에 동의한 날로부터 회원 탈퇴 시까지 적용하는 + 것을 원칙으로 합니다. 단, 이 약관의 일부 조항은 회원이 탈퇴 후에도 + 유효하게 적용될 수 있습니다. + +
+
+ + 제 6조(회원가입) + +
+
+ + ① 이용자는 회사가 정한 가입 양식에 따라 회원정보를 기입한 후 이 약관에 + 동의한다는 의사표시를 함으로서 회원가입을 신청합니다. + +
+ + ② 회사는 제1항과 같이 회원으로 가입할 것을 신청한 이용자 중 다음 각 + 호에 해당하지 않는 한 회원으로 등록합니다. + +
+ + 1. 등록 내용에 허위, 기재누락, 오기가 있는 경우 + +
+ + 2. 기타 회원으로 등록하는 것이 서비스의 기술상 현저히 지장이 있다고 + 판단되는 경우 + +
+ + ③ 회원가입계약의 성립 시기는 회사의 승낙이 회원에게 도달한 시점으로 + 합니다. + +
+
+ + 제 7조(개인 정보 보호 및 관리) + +
+
+ + ① 회사는 「개인정보 보호법」 등 관계 법령이 정하는 바에 따라 계정 + 정보를 포함한 회원의 개인정보를 보호하기 위하여 노력합니다. 회원의 + 개인정보 보호 및 사용에 대해서는 회사가 별도로 고지하는 개인정보처리 + 방침에 따릅니다. 다만, 회사가 제공하는 공식 서비스 사이트 이외의 + 링크된 사이트에서는 회사의 개인정보처리 방침이 적용되지 않습니다. + +
+ + ② 제공된 개인정보는 당해 이용자의 동의 없이 목적 외의 이용이나 + 제3자에게 제공할 수 없으며, 이에 대한 모든 책임은 회사가 집니다. + +
+ + ③ 회사는 제2항에 의해 이용자의 동의를 받아야 하는 경우에는 + 개인정보관리 책임자의 신원(소속, 성명 및 전화번호 기타 연락처), 정보의 + 수집목적 및 이용목적, 제3자에 대한 정보 제공 관련사항 등 「정보통신망 + 이용촉진 및 정보보호 등에 관한 법률」 제22조제2항이 규정한 사항을 미리 + 명시하거나 고지해야 하며, 이용자는 언제든지 이 동의를 철회할 수 + 있습니다. + +
+ + ④ 이용자는 언제든지 회사가 가지고 있는 자신의 개인정보에 대해 열람 및 + 오류정정을 요구할 수 있으며, 회사는 이에 대해 지체 없이 필요한 조치를 + 취할 의무를 집니다. + +
+ + ⑤ 회사는 회원이 이용 계약을 해지하거나 회원 자격을 상실하는 경우 당해 + 회원의 개인정보를 지체 없이 파기합니다. + +
+
+ + 제 8조(회원에 대한 통지) + +
+
+ + ① 회사가 회원에 대한 통지를 하는 경우, 회원이 회사에 제공한 이메일 + 주소로 할 수 있습니다. + +
+ + ② 회사는 불특정 다수 회원에 대한 통지의 경우 1주일 이상 서비스 + 페이지의 공지사항에 게시함으로서 개별 통지에 갈음할 수 있습니다. + +
+
+ + 제 9조(서비스 이용) + +
+
+ + ① 회원은 본 약관 및 회사가 정한 규정에 따라 서비스를 이용할 수 + 있습니다. + +
+ + ② 서비스는 연중무휴, 1일 24시간 제공함을 원칙으로 합니다. 다만, 회사의 + 업무상 또는 기술상의 이유로 서비스가 일시 중지될 수 있으며, 운영상의 + 목적으로 회사가 정한 기간에는 서비스가 일시 중지될 수 있습니다. 이 + 경우 회사는 사전 또는 사후에 이를 공지합니다. + +
+ + ③ 회사는 서비스를 일정 범위로 분할하여 각 범위 별로 이용 가능한 시간을 + 별도로 정할 수 있으며, 이 경우 사전에 공지를 통해 그 내용을 알립니다. + +
+
+ + 제 10조(서비스 제공의 변경 및 중지) + +
+
+ + ① 회사는 상당한 이유가 있는 경우에 운영상, 기술상의 필요에 따라 + 제공하고 있는 전부 또는 일부 서비스를 변경하거나 중지할 수 있습니다. + +
+ + ② 서비스의 내용, 이용 방법, 이용 시간에 대하여 변경 또는 서비스 중지의 + 경우에는 변경 또는 중지될 서비스의 내용 및 사유와 일자 등은 그 변경 + 또는 중지 전에 회원에게 통지하여야 합니다. + +
+ + ③ 회사는 무료로 제공되는 서비스의 일부 또는 전부를 회사의 정책 및 + 운영의 필요 상 수정, 중단, 변경할 수 있으며, 이에 대하여 관련법에 + 특별한 규정이 없는 한 회원에게 별도의 보상을 하지 않습니다. + +
+
+ + 제 11조(서비스의 이용제한 및 정지) + +
+
+ + ① 회사는 회원이 본 약관의 의무를 위반하거나 서비스의 정상적인 운영을 + 방해한 경우, 서비스 이용을 제한하거나 중지할 수 있습니다. + +
+ + ② 전항에 따라 서비스 이용을 제한하거나 중지한 경우 그 사유 및 제한 + 기간 등을 회원에게 통지합니다. + +
+ + ③ 회사는 회원이 계속해서 1년 이상 로그인하지 않는 경우, 회원정보의 + 보호 및 운영의 효율성을 위해 이용을 제한할 수 있습니다. + +
+
+ + 제 12조(회사의 의무) + +
+
+ + ① 회사는 관련 법령과 본 약관이 금지하거나 공서양속에 반하는 행위를 + 하지 않으며, 계속적이고 안정적으로 서비스를 제공하기 위하여 최선을 + 다하여 노력합니다. + +
+ + ② 회사는 회원이 안전하게 서비스를 이용할 수 있도록 개인정보(신용정보 + 포함) 보호를 위해 보안 시스템을 갖추어야 하며 개인정보처리방침을 + 공시하고 준수합니다. + +
+ + ③ 회사는 회원이 수신 동의를 하지 않은 영리 목적의 광고성 전자 우편, + SMS 문자 메시지 등을 발송하지 않습니다. + +
+ + ④ 회사는 회원이 서비스를 이용함에 있어 회사의 고의 또는 중대한 과실로 + 인하여 입은 손해를 배상할 책임을 부담합니다. + +
+
+ + 제 13조(회원의 의무) + +
+
+ + ① 회원은 다음 행위를 하여서는 안 됩니다. + +
+ + 1. 신청 또는 변경 시 허위 내용의 등록 + +
+ + 2. 타인의 정보 도용 + +
+ + 3. 회사가 게시한 정보의 변경 + +
+ + 4. 회사가 정한 정보 이외의 정보(컴퓨터 프로그램 등) 등의 송신 또는 + 게시 + +
+ + 5. 회사와 기타 제3자의 저작권 등 지적재산권에 대한 침해 + +
+ + 6. 회사 및 기타 제3자의 명예를 손상시키거나 업무를 방해하는 행위 + +
+ + 7. 외설 또는 폭력적인 메시지, 화상, 음성, 기타 공서양속에 반하는 + 정보를 서비스에 공개 또는 게시하는 행위 + +
+ + 8. 회사의 동의 없이 영리를 목적으로 서비스를 사용하는 행위 + +
+ + ② 회원은 관계법, 본 약관의 규정, 이용안내 및 서비스와 관련하여 공지한 + 주의사항, 회사가 통지하는 사항 등을 준수하여야 하며, 기타 회사의 + 업무에 방해되는 행위를 하여서는 안 됩니다. + +
+
+ + 제 14조(유료 서비스의 이용) + +
+
+ + ① 회사는 유료 서비스의 이용요금을 회사가 정한 정책에 따라 청구하며, + 회원은 회사가 정한 정책에 따라 이용요금을 납부합니다. + +
+ + ② 회원이 유료 서비스를 이용하는 경우, 회사는 회원이 선택한 결제 방식에 + 따라 이용요금을 청구합니다. + +
+ + ③ 회원이 선택한 결제 방식에 따라 결제하는 경우, 회원은 결제 대행 + 업체가 정하는 정책 및 절차에 따라야 합니다. + +
+ + ④ 유료 서비스의 이용요금, 결제 방법, 결제 시기 등에 관련된 사항은 + 회사가 정한 정책에 따르며, 회사는 이를 회원에게 사전에 공지합니다. + +
+ + ⑤ 회사는 유료 서비스의 결제와 관련된 회원의 문의, 이의 제기 등에 + 성실히 응답하여야 합니다. + +
+ + ⑥ 회원이 이용요금을 연체한 경우, 회사는 회원에 대하여 서비스 이용을 + 제한할 수 있으며, 연체된 이용요금에 대해 별도의 지연손해금을 청구할 수 + 있습니다. + +
+
+ + 제 15조(면책조항) + +
+
+ + ① 회사는 천재지변, 전쟁, 테러, 폭동, 정부의 규제, 전염병, 통신사업자의 + 서비스 중지 등 불가항력적인 사유로 인하여 서비스를 제공할 수 없는 + 경우, 서비스 제공에 대한 책임이 면제됩니다. + +
+ + ② 회사는 회원의 귀책 사유로 인한 서비스의 이용 장애에 대하여 책임을 + 지지 않습니다. + +
+ + ③ 회사는 회원이 서비스와 관련하여 게재한 정보, 자료, 사실의 신뢰도, + 정확성 등 내용에 대해서는 책임을 지지 않습니다. + +
+ + ④ 회사는 무료로 제공되는 서비스 이용과 관련하여 관련법에 특별한 규정이 + 없는 한 책임을 지지 않습니다. + +
+
+ + 제 16조(준거법 및 관할법원) + +
+
+ + ① 본 약관의 해석 및 적용에 관하여는 대한민국 법을 준거법으로 합니다. + +
+ + ② 서비스 이용과 관련하여 회사와 회원 간에 발생한 분쟁에 대해서는 + 민사소송법 상의 관할 법원에 제소합니다. + +
+
+ + 부칙 + +
+ + ① 이 약관은 2024년 6월 11일부터 시행됩니다. + +
+ + ② 2023년 6월 11일부터 시행되던 종전의 약관은 본 약관으로 대체합니다. + +
+
+
+ ) +} + +export default TermsOfUsePage diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.css.ts b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.css.ts new file mode 100644 index 0000000..db33fd2 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css' + +export const onboardingHeader = style({ + marginBottom: 8, +}) + +export const stepArea = style({ + marginBottom: 40, +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.tsx new file mode 100644 index 0000000..68b7996 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/OnboardingHeader.tsx @@ -0,0 +1,24 @@ +'use client' + +import { Step, Text } from '@vook-client/design-system' + +import { onboardingHeader, stepArea } from './OnboardingHeader.css' + +interface OnboardingHeaderProps { + step: number +} + +export const OnboardingHeader = ({ step }: OnboardingHeaderProps) => { + return ( +
+
+ +
+
+ + Onboarding + +
+
+ ) +} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/index.ts b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/index.ts new file mode 100644 index 0000000..5e44f6e --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/OnboardingHeader/index.ts @@ -0,0 +1 @@ +export { OnboardingHeader } from './OnboardingHeader' diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.css.ts b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.css.ts new file mode 100644 index 0000000..13fb65f --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css' + +export const selectBoxGroup = style({ + display: 'grid', + gridTemplateColumns: 'repeat(4, 1fr)', + gap: 20, +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.tsx b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.tsx new file mode 100644 index 0000000..7d1acab --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/SelectBoxGroup.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from 'react' + +import { selectBoxGroup } from './SelectBoxGroup.css' + +export const SelectBoxGroup = ({ children }: PropsWithChildren) => { + return
{children}
+} diff --git a/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/index.ts b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/index.ts new file mode 100644 index 0000000..051d226 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_components/SelectBoxGroup/index.ts @@ -0,0 +1 @@ +export { SelectBoxGroup } from './SelectBoxGroup' diff --git a/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.spec.tsx b/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.spec.tsx new file mode 100644 index 0000000..f214dbd --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.spec.tsx @@ -0,0 +1,61 @@ +import { act, renderHook } from '@testing-library/react' +import { OnboardingFunnel, OnboardingJob } from '@vook-client/api' + +import { OnBoardingProvider, useOnBoarding } from './useOnboarding' + +describe('useOnboarding test', () => { + it('useOnboarding은 useOnboardingContext 내에서 호출되어야 한다.', () => { + expect(() => { + renderHook(useOnBoarding, { + wrapper: ({ children }) =>
{children}
, + }) + }).toThrow('useOnBoarding은 OnBoardingProvider 내에서 사용되어야 합니다.') + }) + + it('funnel과 job의 초기값은 null이다.', () => { + // given + const { result } = renderHook(useOnBoarding, { + wrapper: ({ children }) => ( + {children} + ), + }) + + // when & then + expect(result.current.funnel).toBeNull() + expect(result.current.job).toBeNull() + }) + + it('setFunnel 호출시 funnel이 변경된다.', () => { + // given + const { result } = renderHook(useOnBoarding, { + wrapper: ({ children }) => ( + {children} + ), + }) + + // when + act(() => { + result.current.setFunnel(OnboardingFunnel.BLOG) + }) + + // when & then + expect(result.current.funnel).toBe(OnboardingFunnel.BLOG) + }) + + it('setJob 호출시 job이 변경된다.', () => { + // given + const { result } = renderHook(useOnBoarding, { + wrapper: ({ children }) => ( + {children} + ), + }) + + // when + act(() => { + result.current.setJob(OnboardingJob.DEVELOPER) + }) + + // when & then + expect(result.current.job).toBe(OnboardingJob.DEVELOPER) + }) +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.tsx b/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.tsx new file mode 100644 index 0000000..1e0bef1 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/_context/useOnboarding.tsx @@ -0,0 +1,38 @@ +'use client' + +import { OnboardingFunnel, OnboardingJob } from '@vook-client/api' +import { PropsWithChildren, createContext, useContext, useState } from 'react' + +interface OnBoardingContextType { + funnel: OnboardingFunnel | null + job: OnboardingJob | null + setFunnel: (funnel: OnboardingFunnel | null) => void + setJob: (job: OnboardingJob | null) => void +} + +const onBoardingContext = createContext( + undefined, +) + +export const OnBoardingProvider = ({ children }: PropsWithChildren) => { + const [funnel, setFunnel] = useState(null) + const [job, setJob] = useState(null) + + return ( + + {children} + + ) +} + +export const useOnBoarding = () => { + const context = useContext(onBoardingContext) + + if (!context) { + throw new Error( + 'useOnBoarding은 OnBoardingProvider 내에서 사용되어야 합니다.', + ) + } + + return context +} diff --git a/apps/web/src/app/(onboarding)/onboarding/funnel/page.css.ts b/apps/web/src/app/(onboarding)/onboarding/funnel/page.css.ts new file mode 100644 index 0000000..382cdc5 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/funnel/page.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css' + +import { appearBottom } from '@/styles/animations.css' + +export const header = style({ + marginBottom: 40, + opacity: 0, + + animation: `${appearBottom} 0.5s ease-out forwards`, + animationDelay: '0.7s', +}) + +export const funnelGroup = style({ + marginBottom: 80, + opacity: 0, + + animation: `${appearBottom} 0.5s ease-out forwards`, + animationDelay: '1s', +}) + +export const buttonGroup = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: 16, +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/funnel/page.spec.tsx b/apps/web/src/app/(onboarding)/onboarding/funnel/page.spec.tsx new file mode 100644 index 0000000..93dc013 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/funnel/page.spec.tsx @@ -0,0 +1,70 @@ +import { renderer } from '@/utils/testing' + +import { OnBoardingProvider } from '../_context/useOnboarding' + +import OnboardingFunnelPage from './page' + +describe('OnboardingFunnelPage test', () => { + it('유입 경로를 선택하지 않으면 다음 버튼이 비활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const nextButton = await findByRole('button', { + name: '다음', + }) + + // when + await user.click(nextButton) + + // then + expect(nextButton).toBeDisabled() + }) + + it('유입 경로를 선택하면 해당 유입 경로가 선택되며 다음 버튼이 활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const facebookBox = await findByRole('checkbox', { + name: '페이스북', + }) + const nextButton = await findByRole('button', { + name: '다음', + }) + + // when + await user.click(facebookBox) + + // then + expect(facebookBox).toBeChecked() + expect(nextButton).toBeEnabled() + }) + + it('유입 경로를 재선택하면 해당 유입 경로가 선택되며 기존 유입 경로가 비활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const facebookBox = await findByRole('checkbox', { + name: '페이스북', + }) + const linkedInBox = await findByRole('checkbox', { + name: '링크드인', + }) + + // when + await user.click(facebookBox) + await user.click(linkedInBox) + + // then + expect(facebookBox).not.toBeChecked() + expect(linkedInBox).toBeChecked() + }) +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/funnel/page.stories.tsx b/apps/web/src/app/(onboarding)/onboarding/funnel/page.stories.tsx new file mode 100644 index 0000000..29e770c --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/funnel/page.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +import { OnBoardingProvider } from '../_context/useOnboarding' + +import OnboardingFunnel from './page' + +const meta: Meta = { + title: 'OnboardingFunnel', + component: OnboardingFunnel, + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const Preview: Story = {} diff --git a/apps/web/src/app/(onboarding)/onboarding/funnel/page.tsx b/apps/web/src/app/(onboarding)/onboarding/funnel/page.tsx new file mode 100644 index 0000000..4b99a63 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/funnel/page.tsx @@ -0,0 +1,118 @@ +'use client' + +import { + Button, + ButtonProps, + SelectBox, + Text, +} from '@vook-client/design-system' +import React from 'react' +import clsx from 'clsx' +import { OnboardingFunnel } from '@vook-client/api' + +import { Link } from '@/components/Link' +import { appearBottom } from '@/styles/animations.css' + +import { SelectBoxGroup } from '../_components/SelectBoxGroup' +import { useOnBoarding } from '../_context/useOnboarding' +import { OnboardingHeader } from '../_components/OnboardingHeader' + +import { buttonGroup, funnelGroup, header } from './page.css' + +const FUNNELS: Array<{ + icon: ButtonProps['prefixIcon'] + content: string + funnel: OnboardingFunnel +}> = [ + { + icon: 'X', + content: '엑스', + funnel: OnboardingFunnel.X, + }, + { + icon: 'facebook', + content: '페이스북', + funnel: OnboardingFunnel.FACEBOOK, + }, + { + icon: 'linkedin', + content: '링크드인', + funnel: OnboardingFunnel.LINKEDIN, + }, + { + icon: 'instagram-color', + content: '인스타그램', + funnel: OnboardingFunnel.INSTAGRAM, + }, + { + icon: 'blog-color', + content: '네이버 블로그', + funnel: OnboardingFunnel.BLOG, + }, + { + icon: 'silhouette', + content: '친구/지인 추천', + funnel: OnboardingFunnel.RECOMMENDATION, + }, + { + icon: 'speech-bulloon', + content: '기타', + funnel: OnboardingFunnel.OTHER, + }, +] + +const OnboardingFunnelPage = () => { + const { setFunnel, funnel: selectedFunnel } = useOnBoarding() + + const onClinkFunnel = (funnel: OnboardingFunnel) => { + if (funnel === selectedFunnel) { + setFunnel(null) + return + } + setFunnel(funnel) + } + + return ( +
+ +
+ + Vook를 알게 된 경로를 알려주세요. + +
+
+ + {FUNNELS.map(({ icon, content, funnel }) => ( + onClinkFunnel(funnel)} + selected={funnel === selectedFunnel} + key={content} + prefixIcon={icon} + > + {content} + + ))} + +
+
+ + + 건너뛰기 + + + + + +
+
+ ) +} + +export default OnboardingFunnelPage diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.css.ts b/apps/web/src/app/(onboarding)/onboarding/job/page.css.ts new file mode 100644 index 0000000..fb2d349 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/job/page.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css' + +import { appearBottom } from '@/styles/animations.css' + +export const header = style({ + marginBottom: 40, + opacity: 0, + + animation: `${appearBottom} 0.5s ease-out forwards`, + animationDelay: '0.7s', +}) + +export const jobGroup = style({ + marginBottom: 80, + opacity: 0, + + animation: `${appearBottom} 0.5s ease-out forwards`, + animationDelay: '1s', +}) + +export const buttonGroup = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: 16, +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.spec.tsx b/apps/web/src/app/(onboarding)/onboarding/job/page.spec.tsx new file mode 100644 index 0000000..5a3d002 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/job/page.spec.tsx @@ -0,0 +1,92 @@ +import { renderer } from '@/utils/testing' + +import { OnBoardingProvider } from '../_context/useOnboarding' + +import OnboardingJobPage from './page' + +describe('OnboardingJobPage test', () => { + it('직업을 선택하지 않으면 시작하기 버튼이 비활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const nextButton = await findByRole('button', { + name: '시작하기', + }) + + // when + await user.click(nextButton) + + // then + expect(nextButton).toBeDisabled() + }) + + it('직업을 선택하면 해당 유입 경로가 선택되며 시작하기 버튼이 활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const developerBox = await findByRole('checkbox', { + name: '개발자', + }) + const nextButton = await findByRole('button', { + name: '시작하기', + }) + + // when + await user.click(developerBox) + + // then + expect(developerBox).toBeChecked() + expect(nextButton).toBeEnabled() + }) + + it('직업 재선택하면 해당 직업이 선택되며 기존 직업이 비활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const developerBox = await findByRole('checkbox', { + name: '개발자', + }) + const designerBox = await findByRole('checkbox', { + name: '디자이너', + }) + + // when + await user.click(developerBox) + await user.click(designerBox) + + // then + expect(developerBox).not.toBeChecked() + expect(designerBox).toBeChecked() + }) + + it('온보딩 제출 중에는 시작하기가 비활성화된다.', async () => { + // given + const { user, findByRole } = renderer( + + + , + ) + const developerBox = await findByRole('checkbox', { + name: '개발자', + }) + const nextButton = await findByRole('button', { + name: '시작하기', + }) + + // when + await user.click(developerBox) + await user.click(nextButton) + + // then + expect(nextButton).toBeDisabled() + }) +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.stories.tsx b/apps/web/src/app/(onboarding)/onboarding/job/page.stories.tsx new file mode 100644 index 0000000..c9bbd59 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/job/page.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' + +import { OnBoardingProvider } from '../_context/useOnboarding' + +import OnboardingJobPage from './page' + +const meta: Meta = { + title: 'OnboardingJob', + component: OnboardingJobPage, + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], +} + +export default meta + +type Story = StoryObj + +export const Preview: Story = {} diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.tsx b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx new file mode 100644 index 0000000..ff044c9 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx @@ -0,0 +1,146 @@ +'use client' + +import { + Button, + ButtonProps, + SelectBox, + Text, +} from '@vook-client/design-system' +import React from 'react' +import { OnboardingJob, useOnboardingMutation } from '@vook-client/api' +import { useRouter } from 'next/navigation' + +import { Link } from '@/components/Link' + +import { SelectBoxGroup } from '../_components/SelectBoxGroup' +import { useOnBoarding } from '../_context/useOnboarding' +import { OnboardingHeader } from '../_components/OnboardingHeader' + +import { buttonGroup, header, jobGroup } from './page.css' + +const JOBS: Array<{ + icon: ButtonProps['prefixIcon'] + content: string + job: OnboardingJob +}> = [ + { + icon: 'pencil', + content: '기획자', + job: OnboardingJob.PLANNER, + }, + { + icon: 'artist-pallete', + content: '디자이너', + job: OnboardingJob.DESIGNER, + }, + { + icon: 'laptop', + content: '개발자', + job: OnboardingJob.DEVELOPER, + }, + { + icon: 'light-bulb', + content: '마케터', + job: OnboardingJob.MARKETER, + }, + { + icon: 'sunglass', + content: 'CEO', + job: OnboardingJob.CEO, + }, + { + icon: 'sparkles', + content: 'HR', + job: OnboardingJob.HR, + }, + { + icon: 'speech-bulloon', + content: '기타', + job: OnboardingJob.OTHER, + }, +] + +const OnboardingJobPage = () => { + const router = useRouter() + const { setJob, job: selectedJob, funnel } = useOnBoarding() + + const mutation = useOnboardingMutation( + { + funnel, + job: selectedJob, + }, + { + onSuccess: () => { + router.push('/') + }, + }, + ) + + const onSubmitFunnel = () => { + mutation.mutate() + } + + const onClickJob = (job: OnboardingJob) => { + if (job === selectedJob) { + setJob(null) + return + } + + setJob(job) + } + + return ( +
+ +
+ + 선택한 직업에 맞춰 기본 용어집을 생성합니다. + +
+
+ + {JOBS.map(({ icon, content, job }) => ( + onClickJob(job)} + key={content} + prefixIcon={icon} + selected={job === selectedJob} + > + {content} + + ))} + +
+
+ + + 건너뛰기 + + + + + +
+
+ ) +} + +export default OnboardingJobPage diff --git a/apps/web/src/app/(onboarding)/onboarding/layout.css.ts b/apps/web/src/app/(onboarding)/onboarding/layout.css.ts new file mode 100644 index 0000000..6f718a7 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/layout.css.ts @@ -0,0 +1,44 @@ +import { keyframes, style } from '@vanilla-extract/css' +import { vars } from '@vook-client/design-system' + +import { appearBottom } from '@/styles/animations.css' + +const backgroundColorChange = keyframes({ + '0%': { + backgroundColor: vars.colors['common-white'], + }, + '100%': { + backgroundColor: vars.colors['palette-primary-50'], + }, +}) + +export const onboardingLayout = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + width: '100dvw', + height: '100dvh', + + animation: `${backgroundColorChange} 1s forwards`, +}) + +export const onboardingContainer = style({ + width: 'fit-content', + height: 'fit-content', + + minWidth: 980, + minHeight: 610, + + padding: 100, + + backgroundColor: vars.colors['common-white'], + + borderRadius: 16, + + opacity: 0, + transform: 'translateY(30px)', + + animation: `${appearBottom} 0.3s ease-out forwards`, + animationDelay: '0.2s', +}) diff --git a/apps/web/src/app/(onboarding)/onboarding/layout.tsx b/apps/web/src/app/(onboarding)/onboarding/layout.tsx new file mode 100644 index 0000000..cb36d40 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/layout.tsx @@ -0,0 +1,36 @@ +import React, { PropsWithChildren } from 'react' +import { cookies } from 'next/headers' +import { userInfoService } from '@vook-client/api' +import { redirect } from 'next/navigation' + +import { onboardingContainer, onboardingLayout } from './layout.css' +import { OnBoardingProvider } from './_context/useOnboarding' + +const Layout = async ({ children }: PropsWithChildren) => { + const cookieStore = cookies() + const accessToken = cookieStore.get('access')?.value + const refreshToken = cookieStore.get('refresh')?.value + + if (!accessToken && !refreshToken) { + redirect('/login') + } + + const userInfo = await userInfoService.getUserInfo({ + access: accessToken || '', + refresh: refreshToken || '', + }) + + if (userInfo.result.onboardingCompleted) { + redirect('/') + } + + return ( + +
+
{children}
+
+
+ ) +} + +export default Layout diff --git a/apps/web/src/app/(onboarding)/onboarding/page.tsx b/apps/web/src/app/(onboarding)/onboarding/page.tsx new file mode 100644 index 0000000..db412f6 --- /dev/null +++ b/apps/web/src/app/(onboarding)/onboarding/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from 'next/navigation' + +const OnboardingPage = () => { + redirect('/onboarding/funnel') +} + +export default OnboardingPage diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index f27c3e7..f82fa84 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,7 +1,9 @@ -import { Metadata } from 'next' import '@/styles/reset.css' import '@vook-client/design-system/style.css' +import { Metadata } from 'next' +import { baseFetcher } from '@vook-client/api' + import ReactQueryProvider from '@/providers/ReactQueryProvider' import { pretendard } from '@/styles/fonts' @@ -10,6 +12,10 @@ export const metadata: Metadata = { description: 'Generated by create next app', } +baseFetcher.setUnAuthorizationHandler(() => { + // redirect(`${process.env.NEXT_PUBLIC_DOMAIN}/login`) +}) + const RootLayout = ({ children, }: Readonly<{ @@ -18,7 +24,9 @@ const RootLayout = ({ return ( - {children} + +
{children}
+
) diff --git a/apps/web/src/components/Link/Link.tsx b/apps/web/src/components/Link/Link.tsx new file mode 100644 index 0000000..8c9e2eb --- /dev/null +++ b/apps/web/src/components/Link/Link.tsx @@ -0,0 +1,8 @@ +import React, { PropsWithChildren } from 'react' +import ActualLink, { LinkProps } from 'next/link' + +export const Link = (props: LinkProps & PropsWithChildren) => { + const isStorybook = process.env.IS_STORYBOOK !== undefined + + return +} diff --git a/apps/web/src/components/Link/index.ts b/apps/web/src/components/Link/index.ts new file mode 100644 index 0000000..2ebb60a --- /dev/null +++ b/apps/web/src/components/Link/index.ts @@ -0,0 +1 @@ +export { Link } from './Link' diff --git a/apps/web/src/components/LoginForm/LoginForm.css.ts b/apps/web/src/components/LoginForm/LoginForm.css.ts new file mode 100644 index 0000000..c2647d9 --- /dev/null +++ b/apps/web/src/components/LoginForm/LoginForm.css.ts @@ -0,0 +1,20 @@ +import { style } from '@vanilla-extract/css' + +export const loginForm = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', +}) + +export const loginFormHeader = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 8, + marginBottom: 80, +}) + +export const loginButton = style({ + width: '100%', +}) diff --git a/apps/web/src/components/LoginForm/LoginForm.spec.tsx b/apps/web/src/components/LoginForm/LoginForm.spec.tsx new file mode 100644 index 0000000..3cfccd9 --- /dev/null +++ b/apps/web/src/components/LoginForm/LoginForm.spec.tsx @@ -0,0 +1,14 @@ +import { render } from '@testing-library/react' + +import { LoginForm } from './LoginForm' + +describe('LoginForm test', () => { + it('구글 로그인 버튼을 누르면 구글 로그인 페이지로 이동한다.', () => { + // given + const { getByRole } = render() + const googleLoginButton = getByRole('link') as HTMLAnchorElement + + // when & then + expect(googleLoginButton.href).toContain('/oauth2/authorization/google') + }) +}) diff --git a/apps/web/src/components/LoginForm/LoginForm.stories.tsx b/apps/web/src/components/LoginForm/LoginForm.stories.tsx new file mode 100644 index 0000000..0a56dac --- /dev/null +++ b/apps/web/src/components/LoginForm/LoginForm.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { LoginForm } from './LoginForm' + +const meta = { + title: 'LoginForm', + component: LoginForm, + parameters: { + layout: 'centered', + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Preview: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} diff --git a/apps/web/src/components/LoginForm/LoginForm.tsx b/apps/web/src/components/LoginForm/LoginForm.tsx new file mode 100644 index 0000000..852d911 --- /dev/null +++ b/apps/web/src/components/LoginForm/LoginForm.tsx @@ -0,0 +1,31 @@ +import { Button, Text } from '@vook-client/design-system' + +import { loginButton, loginForm, loginFormHeader } from './LoginForm.css' + +export const LoginForm = () => { + return ( +
+
+ + 등록 또는 로그인 + + + 용어를 가장 쉽고 빠르게 찾는 방법 + +
+ + + +
+ ) +} diff --git a/apps/web/src/components/LoginForm/index.ts b/apps/web/src/components/LoginForm/index.ts new file mode 100644 index 0000000..fc366a7 --- /dev/null +++ b/apps/web/src/components/LoginForm/index.ts @@ -0,0 +1 @@ +export { LoginForm } from './LoginForm' diff --git a/apps/web/src/components/Sidebar/Sidebar.css.ts b/apps/web/src/components/Sidebar/Sidebar.css.ts new file mode 100644 index 0000000..76119ed --- /dev/null +++ b/apps/web/src/components/Sidebar/Sidebar.css.ts @@ -0,0 +1,16 @@ +import { style } from '@vanilla-extract/css' + +import { SIDE_BAR_WIDTH } from '../../styles/layout' + +export const sideBar = style({ + position: 'fixed', + top: 0, + left: 0, + + width: SIDE_BAR_WIDTH, + height: '100dvh', + + padding: 30, + + borderRight: '1px solid black', +}) diff --git a/apps/web/src/components/Sidebar/Sidebar.stories.tsx b/apps/web/src/components/Sidebar/Sidebar.stories.tsx new file mode 100644 index 0000000..b00719d --- /dev/null +++ b/apps/web/src/components/Sidebar/Sidebar.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from '@storybook/react' + +import { Sidebar } from '.' + +const meta = { + title: 'Sidebar', + component: Sidebar, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Preview: Story = {} diff --git a/apps/web/src/components/Sidebar/Sidebar.tsx b/apps/web/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..56de32f --- /dev/null +++ b/apps/web/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,23 @@ +import { Text, TypoLogo } from '@vook-client/design-system' + +import { sideBar } from './Sidebar.css' + +export const Sidebar = () => { + return ( + + ) +} diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx new file mode 100644 index 0000000..70fb43e --- /dev/null +++ b/apps/web/src/components/Sidebar/index.tsx @@ -0,0 +1 @@ +export { Sidebar } from './Sidebar' diff --git a/apps/web/src/components/SignUpForm/SignUpForm.css.ts b/apps/web/src/components/SignUpForm/SignUpForm.css.ts new file mode 100644 index 0000000..f72b784 --- /dev/null +++ b/apps/web/src/components/SignUpForm/SignUpForm.css.ts @@ -0,0 +1,50 @@ +import { style } from '@vanilla-extract/css' +import { vars } from '@vook-client/design-system' + +export const signUpForm = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + + width: '100%', + height: 'fit-content', + + padding: '20px 10px', +}) + +export const signUpFormHeader = style({ + marginBottom: 80, +}) + +export const signUpInputField = style({ + display: 'flex', + flexDirection: 'column', + gap: 30, + width: '100%', + marginBottom: 40, +}) + +export const signUpCheckboxField = style({ + display: 'flex', + flexDirection: 'column', + gap: 14, + width: '100%', + marginBottom: 40, +}) + +export const checkboxGroup = style({ + display: 'flex', + gap: 10, +}) + +export const termsLink = style({ + textDecoration: 'none', + borderBottom: `1px solid ${vars.colors['link-blue']}`, + color: vars.colors['link-blue'], +}) + +export const divider = style({ + width: '100%', + height: 1, + backgroundColor: vars.colors['semantic-line-normal'], +}) diff --git a/apps/web/src/components/SignUpForm/SignUpForm.spec.tsx b/apps/web/src/components/SignUpForm/SignUpForm.spec.tsx new file mode 100644 index 0000000..e03ead6 --- /dev/null +++ b/apps/web/src/components/SignUpForm/SignUpForm.spec.tsx @@ -0,0 +1,140 @@ +import { screen } from '@testing-library/react' + +import { renderer } from '@/utils/testing' + +import { SignUpForm } from './SignUpForm' + +describe('SignUpForm test', () => { + const getFormElements = async () => { + const nicknameInput = await screen.findByLabelText('닉네임') + const termsOfUseButton = + await screen.findByLabelText('이용 약관 동의(필수)') + const policyButton = + await screen.findByLabelText('개인정보 이용 수집 동의(필수)') + const marketingButton = + await screen.findByLabelText('마케팅 메일 수신 동의(선택)') + const submitButton = await screen.findByRole('button') + + return { + nicknameInput, + termsOfUseButton, + policyButton, + marketingButton, + submitButton, + } + } + + it.each([ + { nickname: 'a'.repeat(10) }, + { nickname: 'a'.repeat(11) }, + { nickname: 'a'.repeat(13) }, + ])('닉네임은 10자 이상으로 작성할 수 없다.', async ({ nickname }) => { + // given + const { user } = renderer() + const { nicknameInput } = await getFormElements() + + // when + await user.type(nicknameInput, nickname) + + // then + expect(nicknameInput).toHaveValue('aaaaaaaaaa') + }) + + it('이용약관을 동의하지 않으면 버튼이 비활성화된다.', async () => { + // given + const { user } = renderer() + const { nicknameInput, termsOfUseButton, submitButton } = + await getFormElements() + + // when + await user.type(nicknameInput, 'nickname') + await user.click(termsOfUseButton) + + // then + expect(submitButton).toBeDisabled() + }) + + it('개인정보 이용 수집을 동의하지 않으면 버튼이 비활성화된다.', async () => { + // given + const { user } = renderer() + const { nicknameInput, policyButton, submitButton } = + await getFormElements() + + // when + await user.type(nicknameInput, 'nickname') + await user.click(policyButton) + + // then + expect(submitButton).toBeDisabled() + }) + + it('닉네임을 입력하지 않으면 버튼이 비활성화된다.', async () => { + // given + const { user } = renderer() + const { termsOfUseButton, policyButton, submitButton } = + await getFormElements() + + // when + await user.click(policyButton) + await user.click(termsOfUseButton) + + // then + expect(submitButton).toBeDisabled() + }) + + it('닉네임과 필수약관을 모두 동의하면 버튼이 활성화된다.', async () => { + // given + const { user } = renderer() + const { nicknameInput, termsOfUseButton, policyButton, submitButton } = + await getFormElements() + + // when + await user.type(nicknameInput, 'nickname') + await user.click(policyButton) + await user.click(termsOfUseButton) + + // then + expect(submitButton).toBeEnabled() + }) + + it('마케팅 메일 수신 약관은 선택사항이다.', async () => { + // given + const { user } = renderer() + const { + nicknameInput, + termsOfUseButton, + policyButton, + marketingButton, + submitButton, + } = await getFormElements() + + // when + await user.type(nicknameInput, 'nickname') + await user.click(policyButton) + await user.click(termsOfUseButton) + await user.click(marketingButton) + + // then + expect(submitButton).toBeEnabled() + + // when + await user.click(marketingButton) + expect(submitButton).toBeEnabled() + }) + + it('양식 제출 중에는 버튼이 비활성화된다.', async () => { + // given + const { user } = renderer() + const { nicknameInput, termsOfUseButton, policyButton, submitButton } = + await getFormElements() + + // when + await user.type(nicknameInput, 'nickname') + await user.click(policyButton) + await user.click(termsOfUseButton) + await user.click(submitButton) + + // then + expect(submitButton).toBeDisabled() + }) +}) diff --git a/apps/web/src/components/SignUpForm/SignUpForm.stories.tsx b/apps/web/src/components/SignUpForm/SignUpForm.stories.tsx new file mode 100644 index 0000000..7004f17 --- /dev/null +++ b/apps/web/src/components/SignUpForm/SignUpForm.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { SignUpForm } from './SignUpForm' + +const meta: Meta = { + title: 'SignUpForm', + parameters: { + layout: 'centered', + }, + component: SignUpForm, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const Preview: Story = {} diff --git a/apps/web/src/components/SignUpForm/SignUpForm.tsx b/apps/web/src/components/SignUpForm/SignUpForm.tsx new file mode 100644 index 0000000..4c32874 --- /dev/null +++ b/apps/web/src/components/SignUpForm/SignUpForm.tsx @@ -0,0 +1,188 @@ +'use client' + +import { Button, Checkbox, Input, Text } from '@vook-client/design-system' +import { useSignUpMutation, useUserInfoQuery } from '@vook-client/api' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { ChangeEventHandler, useMemo } from 'react' +import { useRouter } from 'next/navigation' +import z from 'zod' +import Cookies from 'js-cookie' + +import { + checkboxGroup, + divider, + signUpCheckboxField, + signUpForm, + signUpFormHeader, + signUpInputField, + termsLink, +} from './SignUpForm.css' + +const signUpSchema = z.object({ + nickname: z.string().min(1).max(10), + requiredTermsAgree: z.literal(true), + policyAgree: z.literal(true), + marketingEmailOptIn: z.boolean(), +}) + +export const SignUpForm = () => { + const access = Cookies.get('access') + const refresh = Cookies.get('refresh') + + const userInfoQuery = useUserInfoQuery({ + access: access || '', + refresh: refresh || '', + }) + + const { register, handleSubmit, setValue, watch, formState } = useForm({ + resolver: zodResolver(signUpSchema), + defaultValues: { + nickname: '', + requiredTermsAgree: false, + policyAgree: false, + marketingEmailOptIn: false, + }, + }) + const router = useRouter() + + const signUpMutation = useSignUpMutation( + { + nickname: watch('nickname'), + requiredTermsAgree: watch('requiredTermsAgree'), + marketingEmailOptIn: watch('marketingEmailOptIn'), + }, + { + onSuccess: () => { + router.push('/onboarding') + }, + }, + ) + + const isAllChecked = useMemo( + () => + watch('requiredTermsAgree') && + watch('policyAgree') && + watch('marketingEmailOptIn'), + [watch], + ) + + const isSubmitting = useMemo( + () => signUpMutation.isPending, + [signUpMutation.isPending], + ) + + const canSubmit = useMemo( + () => + !formState.isValid || + signUpMutation.isPending || + signUpMutation.isSuccess, + [formState.isValid, signUpMutation.isPending, signUpMutation.isSuccess], + ) + + if (!userInfoQuery.data) { + return null + } + + const email = userInfoQuery.data.result.email + + const onSubmit = handleSubmit(() => { + signUpMutation.mutate() + }) + + const onCheckAll: ChangeEventHandler = (e) => { + setValue('requiredTermsAgree', e.target.checked, { shouldValidate: true }) + setValue('policyAgree', e.target.checked, { shouldValidate: true }) + setValue('marketingEmailOptIn', e.target.checked, { shouldValidate: true }) + } + + return ( +
+
+ + 회원가입 + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + ) +} diff --git a/apps/web/src/components/SignUpForm/index.ts b/apps/web/src/components/SignUpForm/index.ts new file mode 100644 index 0000000..1891934 --- /dev/null +++ b/apps/web/src/components/SignUpForm/index.ts @@ -0,0 +1 @@ +export { SignUpForm } from './SignUpForm' diff --git a/apps/web/src/components/Term/Term.tsx b/apps/web/src/components/Term/Term.tsx index 7bff678..381677d 100644 --- a/apps/web/src/components/Term/Term.tsx +++ b/apps/web/src/components/Term/Term.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react' import { Button, List, Text } from '@vook-client/design-system' import { SearchSort, searchSort, useSearchQuery } from '@vook-client/api' @@ -61,6 +61,11 @@ const TextContainer = ({ length }: { length?: number }) => { export const Term = () => { const { requestQuery } = searchStore() const [sort, setSort] = useState() + const [minimumDelayDone, setMinimumDelayDone] = useState(false) + + useLayoutEffect(() => { + setTimeout(() => setMinimumDelayDone(true), 500) + }, []) const { data: response, isLoading } = useSearchQuery( // DTO @@ -77,7 +82,7 @@ export const Term = () => { }, ) - const handleSort = (kind: string) => { + const handleSort = useCallback((kind: string) => { setSort((prevSort) => { const ascKey = `${kind}Asc` as keyof typeof searchSort const descKey = `${kind}Desc` as keyof typeof searchSort @@ -87,18 +92,29 @@ export const Term = () => { return searchSort[ascKey] } }) - } + }, []) + + const done = useMemo( + () => (minimumDelayDone && !isLoading) || !response, + [isLoading, minimumDelayDone, response], + ) + const noResult = useMemo( + () => done && response?.result.hits.length === 0, + [done, response?.result.hits.length], + ) + const hasResult = useMemo( + () => ((done && response?.result.hits.length) || 0) > 0, + [done, response?.result.hits.length], + ) return (
- {isLoading ? ( - - ) : response?.result.hits.length === 0 ? ( - - ) : ( + {!done && } + {noResult && } + {hasResult && ( <>
{ 뜻
- {response?.result.hits.map((data, index) => { return (
diff --git a/apps/web/src/components/index.tsx b/apps/web/src/components/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts new file mode 100644 index 0000000..f618073 --- /dev/null +++ b/apps/web/src/middleware.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server' +import { UserInfoResponse, UserStatus } from 'node_modules/@vook-client/api' + +/** + * 권한 검사를 위한 미들웨어 생성 함수 + * + * - 토큰이 모두 없는 경우: destination으로 리다이렉트 + * - access 토큰이 없는 경우: refresh 토큰을 이용해 새로운 access 토큰을 발급 후 권한 확인 및 토큰 갱신 + * - access 토큰이 있지만 유효하지 않은 경우: refresh 토큰을 이용해 새로운 access 토큰을 발급 후 권한 확인 및 토큰 갱신 + * - refresh 토큰이 만료된 경우: destination으로 리다이렉트 + * - 유저 상태가 허용되지 않는 경우: destination으로 리다이렉트 + * + * @param roles 접근 권한이 허용된 유저 상태 + */ +const checkUserStatusMiddleware = + (roles: Array) => + async ( + req: NextRequest, + finalResponse: NextResponse, + destination: string, + ) => { + const accessToken = req.cookies.get('access')?.value + const refreshToken = req.cookies.get('refresh')?.value + const loginRedirectResponse = NextResponse.redirect( + `${process.env.NEXT_PUBLIC_DOMAIN}${destination}`, + ) + + let newAccessToken: string | null = null + let newRefreshToken: string | null = null + + const tokenGenerate = async (refresh: string) => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Refresh-Authorization': refresh, + }, + }, + ) + if (res.ok) { + newAccessToken = res.headers.get('Authorization') + newRefreshToken = res.headers.get('X-Refresh-Authorization') + finalResponse.cookies.set('access', newAccessToken!) + finalResponse.cookies.set('refresh', newRefreshToken!) + } else { + return false + } + + return true + } + + const fetchUserInfo = async (token: string) => { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user/info`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: token, + }, + }) + if (res.ok) { + return res.json() as Promise + } + return null + } + + const isBothTokenMissing = !accessToken && !refreshToken + + if (isBothTokenMissing) { + return loginRedirectResponse + } + + const isAccessTokenMissing = !accessToken && refreshToken + + if (isAccessTokenMissing) { + const success = await tokenGenerate(refreshToken) + + if (!success) { + return loginRedirectResponse + } + } + + let userInfo = await fetchUserInfo(newAccessToken || accessToken || '') + + if (!userInfo) { + const success = await tokenGenerate(refreshToken!) + + if (!success) { + return loginRedirectResponse + } + + userInfo = await fetchUserInfo(newAccessToken || accessToken || '') + + if (!userInfo) { + return loginRedirectResponse + } + } + + if (!roles.includes(userInfo.result.status)) { + return loginRedirectResponse + } + + return finalResponse + } + +const onlyRegisteredMatch = ['/onboarding'] + +const onlyRegisteredMiddleware = checkUserStatusMiddleware([ + UserStatus.Registered, + UserStatus.Withdrawn, +]) + +const onlyUnregisteredSocialUser = checkUserStatusMiddleware([ + UserStatus.SocialLoginCompleted, +]) + +export async function middleware(req: NextRequest) { + const response = NextResponse.next() + + if ( + onlyRegisteredMatch.some((url) => req.nextUrl.pathname.includes(url)) || + req.nextUrl.pathname === '/' + ) { + return onlyRegisteredMiddleware(req, response, '/login') + } + + if (req.nextUrl.pathname === '/signup') { + return onlyUnregisteredSocialUser(req, response, '/login') + } +} diff --git a/apps/web/src/styles/animations.css.ts b/apps/web/src/styles/animations.css.ts new file mode 100644 index 0000000..db6d55d --- /dev/null +++ b/apps/web/src/styles/animations.css.ts @@ -0,0 +1,12 @@ +import { keyframes } from '@vanilla-extract/css' + +export const appearBottom = keyframes({ + '0%': { + opacity: 0, + transform: 'translateY(20px)', + }, + '100%': { + opacity: 1, + transform: 'translateY(0)', + }, +}) diff --git a/apps/web/src/styles/layout.ts b/apps/web/src/styles/layout.ts new file mode 100644 index 0000000..c46eac9 --- /dev/null +++ b/apps/web/src/styles/layout.ts @@ -0,0 +1 @@ +export const SIDE_BAR_WIDTH = 260 diff --git a/apps/web/src/utils/localStorage.ts b/apps/web/src/utils/localStorage.ts new file mode 100644 index 0000000..ea9c1e5 --- /dev/null +++ b/apps/web/src/utils/localStorage.ts @@ -0,0 +1,20 @@ +export const setLocalStorage = (key: string, value: unknown) => { + localStorage.setItem(key, JSON.stringify(value)) +} + +export const getLocalStorage = (key: string): T | null => { + try { + const value = localStorage.getItem(key) + + if (!value) { + return null + } + + const result = JSON.parse(value) as T + return result + } catch (error) { + // eslint-disable-next-line no-console + console.error(error) + return null + } +} diff --git a/apps/web/src/utils/testing.tsx b/apps/web/src/utils/testing.tsx new file mode 100644 index 0000000..de5555b --- /dev/null +++ b/apps/web/src/utils/testing.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react' +import * as RTL from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import userEvent from '@testing-library/user-event' + +export const renderer = (component: ReactNode) => { + const user = userEvent.setup() + + return { + user, + ...RTL.render( + + {component} + , + ), + } +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index c2674c4..5b31436 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -10,7 +10,9 @@ "paths": { "@/providers/*": ["./src/providers/*"], "@/components/*": ["./src/components/*"], - "@/styles/*": ["./src/styles/*"] + "@/styles/*": ["./src/styles/*"], + "@/utils/*": ["./src/utils/*"], + "@/mocks/*": ["./src/mocks/*"] } }, "include": [ @@ -18,7 +20,8 @@ "next.config.mjs", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "src/app/(onboarding)/onboarding/funnel/fsafs" ], "exclude": ["node_modules"] } diff --git a/vitest.config.mts b/apps/web/vitest.config.ts similarity index 57% rename from vitest.config.mts rename to apps/web/vitest.config.ts index ddb39cd..ed6e118 100644 --- a/vitest.config.mts +++ b/apps/web/vitest.config.ts @@ -1,18 +1,21 @@ import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin' import react from '@vitejs/plugin-react' import { configDefaults, defineConfig } from 'vitest/config' +import { config } from 'dotenv' +import tsconfigPaths from 'vite-tsconfig-paths' // https://vitejs.dev/config/ export default defineConfig({ - // @ts-ignore - plugins: [react(), vanillaExtractPlugin()], - mode: 'verbose', + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + plugins: [react(), vanillaExtractPlugin(), tsconfigPaths()], test: { globals: true, environment: 'jsdom', setupFiles: 'setupTests.ts', - reporters: 'verbose', css: true, exclude: [...configDefaults.exclude], + env: { + ...config({ path: './.env.development' }).parsed, + }, }, }) diff --git a/apps/workshop/.storybook/main.ts b/apps/workshop/.storybook/main.ts index bf21c38..fffb0c1 100644 --- a/apps/workshop/.storybook/main.ts +++ b/apps/workshop/.storybook/main.ts @@ -2,12 +2,14 @@ import type { StorybookConfig } from '@storybook/react-vite' import { join, dirname } from 'path' import MiniCssExtractPlugin from 'mini-css-extract-plugin' -import { mergeConfig } from 'vite' +import { loadConfigFromFile, mergeConfig } from 'vite' function getAbsolutePath(value: string): any { return dirname(require.resolve(join(value, 'package.json'))) } +import path from 'path' + const config: StorybookConfig = { stories: [ { @@ -76,11 +78,17 @@ const config: StorybookConfig = { docs: { autodocs: 'tag', }, - staticDirs: ['../public'], - async viteFinal(config, { configType }) { + staticDirs: [ + { + from: '../public', + to: './public', + }, + ], + async viteFinal(config, type) { return mergeConfig(config, { define: { 'process.env.NEXT_PUBLIC_API_URL': false, + 'process.env.IS_STORYBOOK': true, }, plugins: [require('@vanilla-extract/vite-plugin').vanillaExtractPlugin()], esbuild: { diff --git a/apps/workshop/.storybook/manager.ts b/apps/workshop/.storybook/manager.ts new file mode 100644 index 0000000..6e3e837 --- /dev/null +++ b/apps/workshop/.storybook/manager.ts @@ -0,0 +1,15 @@ +import { addons } from '@storybook/manager-api' +import { create } from '@storybook/theming/create' + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Vook Client Workshop', + brandImage: '/logo.png', + + colorPrimary: '#3A10E5', + colorSecondary: '#5D5CE5', + + barTextColor: '#FFFFFF', + }), +}) diff --git a/apps/workshop/.storybook/preview.ts b/apps/workshop/.storybook/preview.ts deleted file mode 100644 index cd2f5cb..0000000 --- a/apps/workshop/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import './storybook.css' - -const preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -} - -export default preview diff --git a/apps/workshop/.storybook/preview.tsx b/apps/workshop/.storybook/preview.tsx new file mode 100644 index 0000000..5eda6de --- /dev/null +++ b/apps/workshop/.storybook/preview.tsx @@ -0,0 +1,44 @@ +import '@vook-client/design-system/style.css' +import './storybook.css' + +import { handlers } from '@vook-client/api' + +import { initialize, mswLoader } from 'msw-storybook-addon' + +import { Preview } from '@storybook/react' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { + AppRouterContext, + type AppRouterInstance, +} from 'next/dist/shared/lib/app-router-context.shared-runtime' + +initialize() + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + msw: { + handlers: { + ...handlers, + }, + }, + }, + loaders: [mswLoader], + decorators: [ + (Story) => ( + + + + + + ), + ], +} + +export default preview diff --git a/apps/workshop/.storybook/storybook.css b/apps/workshop/.storybook/storybook.css index 5acceb2..383114d 100644 --- a/apps/workshop/.storybook/storybook.css +++ b/apps/workshop/.storybook/storybook.css @@ -4,6 +4,10 @@ font-family: 'Pretendard', 'Noto Sans KR', sans-serif; } +a { + text-decoration: none; +} + .storybook-list { display: flex; flex-direction: column; diff --git a/apps/workshop/package.json b/apps/workshop/package.json index b21e655..2028f32 100644 --- a/apps/workshop/package.json +++ b/apps/workshop/package.json @@ -9,9 +9,12 @@ "build-storybook": "storybook build" }, "dependencies": { + "@tanstack/react-query": "^5.32.0", + "@vook-client/api": "*", + "@vook-client/design-system": "*", + "next": "^14.1.1", "react": "^18.3.1", - "react-dom": "^18.3.1", - "ui": "*" + "react-dom": "^18.3.1" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.3", @@ -21,9 +24,11 @@ "@storybook/addon-onboarding": "^8.0.10", "@storybook/addon-styling-webpack": "^1.0.0", "@storybook/blocks": "^8.0.10", + "@storybook/manager-api": "^8.1.7", "@storybook/react": "^8.0.10", "@storybook/react-vite": "^8.0.10", "@storybook/test": "^8.0.10", + "@storybook/theming": "^8.1.7", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vanilla-extract/vite-plugin": "^4.0.9", @@ -33,8 +38,16 @@ "chromatic": "^11.3.2", "css-loader": "^7.1.1", "mini-css-extract-plugin": "^2.9.0", + "msw": "^2.3.1", + "msw-storybook-addon": "^2.0.2", "storybook": "^8.0.10", "style-loader": "^4.0.0", - "vite": "^4.4.5" + "vite": "^4.4.5", + "vite-tsconfig-paths": "^4.3.2" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/apps/workshop/public/Icons/backward.svg b/apps/workshop/public/Icons/backward.svg deleted file mode 100644 index c5f1d86..0000000 --- a/apps/workshop/public/Icons/backward.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/workshop/public/Icons/blog.svg b/apps/workshop/public/Icons/blog.svg deleted file mode 100644 index bd6ba3e..0000000 --- a/apps/workshop/public/Icons/blog.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/apps/workshop/public/Icons/close-circle.svg b/apps/workshop/public/Icons/close-circle.svg deleted file mode 100644 index b59d175..0000000 --- a/apps/workshop/public/Icons/close-circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/workshop/public/Icons/close.svg b/apps/workshop/public/Icons/close.svg deleted file mode 100644 index bc053ff..0000000 --- a/apps/workshop/public/Icons/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/workshop/public/Icons/instagram.svg b/apps/workshop/public/Icons/instagram.svg deleted file mode 100644 index ef79d4a..0000000 --- a/apps/workshop/public/Icons/instagram.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/workshop/public/Icons/plus.svg b/apps/workshop/public/Icons/plus.svg deleted file mode 100644 index 8c150be..0000000 --- a/apps/workshop/public/Icons/plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/workshop/public/Icons/search.svg b/apps/workshop/public/Icons/search.svg deleted file mode 100644 index 4d4ce4e..0000000 --- a/apps/workshop/public/Icons/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/workshop/public/Icons/symbol.svg b/apps/workshop/public/Icons/symbol.svg deleted file mode 100644 index 0706f09..0000000 --- a/apps/workshop/public/Icons/symbol.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/workshop/public/Icons/typo.svg b/apps/workshop/public/Icons/typo.svg deleted file mode 100644 index 04ff31f..0000000 --- a/apps/workshop/public/Icons/typo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/workshop/public/logo.png b/apps/workshop/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..58251ddd606e4e44c21e36f127578d7d323799d7 GIT binary patch literal 984 zcmV;}11J26P)ufwQnH!7OnJRuX6#mvkkeZOO1W zolZ8p#}&a@@Bo{IfaFT3F&9QQ@;CF0*{rbb;QG&fk>VDH-$BIiWUf=z9FIrG>UzDR z?|b`5v-y>-dg((8>~OWd(Bj(1-n1BgvHKLRfLxefw~M)_U~=~Es@schKXiL#{dN=L z$H-cIU)l6rT{v^k;#7 ziPAG>CBz7UmkqlT6zd~@R|!jHVVZfiq`jb0K%5wl-s_2)Ot20w+}zmhS>FloB}RU{ zhabHdQ`+_j;}5(4;(1TOJF zOGhtm`xkMLziNpv(D`%;R8L`+f;`(wn6vJH^(YbEwK-&s_ASY0F@~*?spvlo+uQja z@mhbvyv!tv!O?Llgu~%*8=US#sOwq=wFXi&863yK!)xIF4-yi;3fP`Kwk@+$tpCFL zTg*33Z*b(uSXI?1Hz+2o4Pxq(XI-*ozvL9|!!$1L->^b0wt<_83iFj<`#bio zKma!)2}Dbqj#cc3f&E|tCIs==pF1BFmXN{?@HN6$coT}UXJFqZ-|~np)!Y-@%p=<~ z{B>NxFRmSn`<-F|xpA;#AIw(p=)T1Ef9{428#Zis0R90FgH13kUTf { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/apps/workshop/tsconfig.json b/apps/workshop/tsconfig.json index 28c6c36..878cde7 100644 --- a/apps/workshop/tsconfig.json +++ b/apps/workshop/tsconfig.json @@ -1,4 +1,13 @@ { "extends": "@vook-client/typescript-config/vite.json", - "include": [".storybook/*"] + "include": [".storybook/*"], + "compilerOptions": { + "paths": { + "@/providers/*": ["./src/providers/*"], + "@/components/*": ["./src/components/*"], + "@/styles/*": ["./src/styles/*"], + "@/utils/*": ["./src/utils/*"], + "@/mocks/*": ["./src/mocks/*"] + } + } } diff --git a/apps/workshop/vite.config.ts b/apps/workshop/vite.config.ts index 5a33944..3e8ce05 100644 --- a/apps/workshop/vite.config.ts +++ b/apps/workshop/vite.config.ts @@ -1,7 +1,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tsconfigPaths from 'vite-tsconfig-paths' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), tsconfigPaths()], }) diff --git a/package.json b/package.json index f460425..5923997 100644 --- a/package.json +++ b/package.json @@ -7,29 +7,20 @@ "dev:extension": "turbo dev --filter=@vook-client/extension", "lint": "turbo lint", "lint-front": "lint-staged", - "test": "vitest", + "test": "turbo test", "prepare": "husky install", "build-storybook": "turbo run build --filter=workshop" }, "devDependencies": { - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^15.0.5", "@titicaca/prettier-config-triple": "^1.1.0", - "@vanilla-extract/vite-plugin": "^4.0.9", - "@vitejs/plugin-react": "^4.2.1", - "@vitest/ui": "^1.6.0", "@vook-client/eslint-config": "*", "@vook-client/typescript-config": "*", "eslint-plugin-vitest": "^0.2.6", "husky": "^9.0.11", - "jsdom": "^24.0.0", "lint-staged": "^15.2.2", "prettier": "^3.2.5", - "react": "^18.3.1", "turbo": "latest", - "typescript": "5.2.2", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.5.2" + "typescript": "5.2.2" }, "prettier": "@titicaca/prettier-config-triple", "engines": { @@ -39,8 +30,5 @@ "workspaces": [ "apps/*", "packages/*" - ], - "dependencies": { - "@testing-library/user-event": "^14.5.2" - } + ] } diff --git a/packages/api/package.json b/packages/api/package.json index fe670c7..8216951 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -6,10 +6,14 @@ "exports": "./src/index.ts", "scripts": {}, "dependencies": { - "@tanstack/react-query": "^5.32.0" + "@tanstack/react-query": "^5.32.0", + "@types/js-cookie": "^3.0.6", + "js-cookie": "^3.0.5" }, "devDependencies": { "@vook-client/eslint-config": "*", - "@vook-client/typescript-config": "*" + "@vook-client/typescript-config": "*", + "msw": "^2.3.1", + "zod": "^3.23.8" } } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a46087f..ea331a2 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,4 +1,10 @@ -export { Fetcher } from './lib/fetcher' +export { baseFetcher, Fetcher } from './lib/fetcher' +export { handlers } from './mocks/handlers' +export { + OnboardingFunnel, + OnboardingJob, +} from './services/useOnboardingMutation/model' +export { useOnboardingMutation } from './services/useOnboardingMutation/queries' export type { SearchDTO, SearchHit, @@ -11,3 +17,12 @@ export { useSearchQuery, } from './services/useSearchQuery/queries' export { searchService } from './services/useSearchQuery/searchService' +export type { SignUpDTO } from './services/useSignUpMutation/model' +export { useSignUpMutation } from './services/useSignUpMutation/queries' +export type { UserInfoResponse } from './services/useUserInfoQuery/model' +export { UserStatus } from './services/useUserInfoQuery/model' +export { + useUserInfoQuery, + useUserInfoSuspenseQuery, +} from './services/useUserInfoQuery/queries' +export { userInfoService } from './services/useUserInfoQuery/userInfoService' diff --git a/packages/api/src/lib/fetcher.ts b/packages/api/src/lib/fetcher.ts index 25b9f8f..b73925c 100644 --- a/packages/api/src/lib/fetcher.ts +++ b/packages/api/src/lib/fetcher.ts @@ -1,6 +1,30 @@ +import Cookies from 'js-cookie' + +import { Tokens } from '../shared/type' + +const API_URL = + process.env.NEXT_PUBLIC_API_URL || + process.env.PLASMO_PUBLIC_API_URL || + 'https://dev.vook-api.seungyeop-lee.com' + export class Fetcher { private baseUrl: string + private unAuthorizationHandler = () => {} + + private changeTokenHandler = (tokens: Tokens) => { + Cookies.set('accessToken', tokens.access) + Cookies.set('refreshToken', tokens.refresh) + } + + public setUnAuthorizationHandler = (handler: VoidFunction) => { + this.unAuthorizationHandler = handler + } + + public setChangeTokenHandler = (handler: (tokens: Tokens) => void) => { + this.changeTokenHandler = handler + } + public constructor(baseUrl: string) { this.baseUrl = baseUrl } @@ -13,19 +37,31 @@ export class Fetcher { try { const fetchOptions: RequestInit = { + ...options, headers: { Accept: 'application/json', 'Content-Type': 'application/json', + ...options?.headers, }, - ...options, } const response = await fetch(`${this.baseUrl}${url}`, fetchOptions) + if (response.status === 401) { + const result = await this.generateNewAccessToken( + url, + fetchOptions, + ) + + if (result) { + return result + } + } + if (response.ok) { data = (await response.json()) as Promise } else { - throw new Error(response.statusText) + throw new Error('네트워크 통신 과정에서 에러가 발생하였습니다!') } } catch (error) { // eslint-disable-next-line no-console @@ -36,6 +72,60 @@ export class Fetcher { return data } + private generateNewAccessToken = async ( + url: string, + options?: RequestInit, + ) => { + const headers = { + ...options?.headers, + } as { + 'X-Refresh-Authorization': string + } + + try { + const response = await fetch(`${API_URL}/auth/refresh`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Refresh-Authorization': headers['X-Refresh-Authorization'], + }, + }) + + const newAccessToken = response.headers.get('Authorization') + const newRefreshToken = response.headers.get('X-Refresh-Authorization') + + if (!newAccessToken || !newRefreshToken) { + throw new Error('토큰 갱신에 실패하였습니다.') + } + + this.changeTokenHandler({ + access: newAccessToken, + refresh: newRefreshToken, + }) + + return this.request(url, { + ...options, + headers: { + ...options?.headers, + Authorization: newAccessToken, + 'X-Refresh-Authorization': newRefreshToken, + }, + }) + } catch (err) { + // eslint-disable-next-line no-console + console.error(err) + + if (global.location) { + global.location.href = '/login' + return + } + + if (this.unAuthorizationHandler) { + this.unAuthorizationHandler() + } + } + } + public async get(url: string, options?: RequestInit) { return this.request(url, options) } @@ -65,9 +155,4 @@ export class Fetcher { } } -const API_URL = - process.env.NEXT_PUBLIC_API_URL || - process.env.PLASMO_PUBLIC_API_URL || - 'https://dev.vook-api.seungyeop-lee.com' - export const baseFetcher = new Fetcher(API_URL) diff --git a/packages/api/src/mocks/config.ts b/packages/api/src/mocks/config.ts new file mode 100644 index 0000000..4d8b666 --- /dev/null +++ b/packages/api/src/mocks/config.ts @@ -0,0 +1 @@ +export const API_URL = 'https://dev.vook-api.seungyeop-lee.com' diff --git a/packages/api/src/mocks/handlers/index.ts b/packages/api/src/mocks/handlers/index.ts new file mode 100644 index 0000000..1119ad2 --- /dev/null +++ b/packages/api/src/mocks/handlers/index.ts @@ -0,0 +1,3 @@ +import { userHandlers } from './user' + +export const handlers = [...userHandlers] diff --git a/packages/api/src/mocks/handlers/user.ts b/packages/api/src/mocks/handlers/user.ts new file mode 100644 index 0000000..351bb1f --- /dev/null +++ b/packages/api/src/mocks/handlers/user.ts @@ -0,0 +1,37 @@ +import { delay, http, HttpResponse } from 'msw' + +import { API_URL } from '../config' +import { + UserInfoResponse, + UserStatus, +} from '../../services/useUserInfoQuery/model' + +export const userHandlers = [ + http.get(`${API_URL}/user/info`, () => { + const res: UserInfoResponse = { + code: '202', + result: { + uid: 'uid', + email: 'dummyuser1234@vook.com', + status: UserStatus.Registered, + onboardingCompleted: false, + nickname: '', + }, + } + return HttpResponse.json(res) + }), + http.post(`${API_URL}/user/register`, async () => { + const res = { + code: 'SUCCESS', + } + await delay(1000) + return HttpResponse.json(res) + }), + http.post(`${API_URL}/user/onboarding`, async () => { + const res = { + code: 'SUCCESS', + } + await delay(1000) + return HttpResponse.json(res) + }), +] diff --git a/packages/api/src/services/useOnboardingMutation/model.ts b/packages/api/src/services/useOnboardingMutation/model.ts new file mode 100644 index 0000000..f45583e --- /dev/null +++ b/packages/api/src/services/useOnboardingMutation/model.ts @@ -0,0 +1,28 @@ +export enum OnboardingFunnel { + X = 'X', + FACEBOOK = 'FACEBOOK', + LINKEDIN = 'LINKEDIN', + INSTAGRAM = 'INSTAGRAM', + BLOG = 'NAVER_BLOG', + RECOMMENDATION = 'RECOMMENDATION', + OTHER = 'OTHER', +} + +export enum OnboardingJob { + PLANNER = 'PLANNER', + DESIGNER = 'DESIGNER', + DEVELOPER = 'DEVELOPER', + MARKETER = 'MARKETER', + CEO = 'CEO', + HR = 'HR', + OTHER = 'OTHER', +} + +export interface OnboardingDTO { + funnel: OnboardingFunnel | null + job: OnboardingJob | null +} + +export interface OnboardingResponse { + code: string +} diff --git a/packages/api/src/services/useOnboardingMutation/onboardingService.ts b/packages/api/src/services/useOnboardingMutation/onboardingService.ts new file mode 100644 index 0000000..4a7a8f2 --- /dev/null +++ b/packages/api/src/services/useOnboardingMutation/onboardingService.ts @@ -0,0 +1,16 @@ +import Cookies from 'js-cookie' + +import { baseFetcher } from '../..' + +import { OnboardingDTO, OnboardingResponse } from './model' + +export const onboardingService = { + async postOnboarding(body: OnboardingDTO) { + return baseFetcher.post('/user/onboarding', { + headers: { + Authorization: Cookies.get('access') || '', + }, + body: JSON.stringify(body), + }) + }, +} diff --git a/packages/api/src/services/useOnboardingMutation/queries.ts b/packages/api/src/services/useOnboardingMutation/queries.ts new file mode 100644 index 0000000..e52acb7 --- /dev/null +++ b/packages/api/src/services/useOnboardingMutation/queries.ts @@ -0,0 +1,20 @@ +import { MutationOptions, useMutation } from '@tanstack/react-query' + +import { OnboardingDTO, OnboardingResponse } from './model' +import { onboardingService } from './onboardingService' + +export const onboardingOptions = { + postOnboarding: (dto: OnboardingDTO) => ({ + mutationFn: () => onboardingService.postOnboarding(dto), + }), +} + +export const useOnboardingMutation = ( + dto: OnboardingDTO, + queryOptions: MutationOptions = {}, +) => { + return useMutation({ + ...onboardingOptions.postOnboarding(dto), + ...queryOptions, + }) +} diff --git a/packages/api/src/services/useSearchQuery/searchService.ts b/packages/api/src/services/useSearchQuery/searchService.ts index b4c111a..71dce86 100644 --- a/packages/api/src/services/useSearchQuery/searchService.ts +++ b/packages/api/src/services/useSearchQuery/searchService.ts @@ -4,7 +4,6 @@ import { SearchDTO, SearchResponse } from './model' export const searchService = { async get(body: SearchDTO) { - await new Promise((resolve) => setTimeout(resolve, 500)) return baseFetcher.post('/demo/terms/search', { body: JSON.stringify(body), }) diff --git a/packages/api/src/services/useSignUpMutation/model.ts b/packages/api/src/services/useSignUpMutation/model.ts new file mode 100644 index 0000000..a80a1e7 --- /dev/null +++ b/packages/api/src/services/useSignUpMutation/model.ts @@ -0,0 +1,9 @@ +export interface SignUpDTO { + nickname: string + requiredTermsAgree: boolean + marketingEmailOptIn: boolean +} + +export interface SignUpResponse { + code: string +} diff --git a/packages/api/src/services/useSignUpMutation/queries.ts b/packages/api/src/services/useSignUpMutation/queries.ts new file mode 100644 index 0000000..4893317 --- /dev/null +++ b/packages/api/src/services/useSignUpMutation/queries.ts @@ -0,0 +1,20 @@ +import { MutationOptions, useMutation } from '@tanstack/react-query' + +import { signUpService } from './signUpService' +import { SignUpDTO, SignUpResponse } from './model' + +export const signUpQueryOptions = { + register: (dto: SignUpDTO) => ({ + mutationFn: () => signUpService.register(dto), + }), +} + +export const useSignUpMutation = ( + dto: SignUpDTO, + queryOptions: MutationOptions = {}, +) => { + return useMutation({ + ...signUpQueryOptions.register(dto), + ...queryOptions, + }) +} diff --git a/packages/api/src/services/useSignUpMutation/signUpService.ts b/packages/api/src/services/useSignUpMutation/signUpService.ts new file mode 100644 index 0000000..a2666a8 --- /dev/null +++ b/packages/api/src/services/useSignUpMutation/signUpService.ts @@ -0,0 +1,16 @@ +import Cookies from 'js-cookie' + +import { baseFetcher } from '../..' + +import { SignUpResponse, SignUpDTO } from './model' + +export const signUpService = { + async register(body: SignUpDTO) { + return baseFetcher.post('/user/register', { + headers: { + Authorization: Cookies.get('access') || '', + }, + body: JSON.stringify(body), + }) + }, +} diff --git a/packages/api/src/services/useUserInfoQuery/model.ts b/packages/api/src/services/useUserInfoQuery/model.ts new file mode 100644 index 0000000..ba4a4c9 --- /dev/null +++ b/packages/api/src/services/useUserInfoQuery/model.ts @@ -0,0 +1,16 @@ +export enum UserStatus { + SocialLoginCompleted = 'SOCIAL_LOGIN_COMPLETED', + Registered = 'REGISTERED', + Withdrawn = 'WITHDRAWN', +} + +export interface UserInfoResponse { + code: string + result: { + uid: string + email: string + status: UserStatus + onboardingCompleted: boolean + nickname: string + } +} diff --git a/packages/api/src/services/useUserInfoQuery/queries.ts b/packages/api/src/services/useUserInfoQuery/queries.ts new file mode 100644 index 0000000..574bdcb --- /dev/null +++ b/packages/api/src/services/useUserInfoQuery/queries.ts @@ -0,0 +1,33 @@ +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' + +import { CustomQueryOptions, Tokens } from '../../shared/type' + +import { userInfoService } from './userInfoService' +import { UserInfoResponse } from './model' + +export const userInfoQueryOptions = { + getUserInfo: (token: Tokens) => ({ + queryKey: [], + queryFn: () => userInfoService.getUserInfo(token), + }), +} + +export const useUserInfoQuery = ( + token: Tokens, + queryOptions: CustomQueryOptions = {}, +) => { + return useQuery({ + ...userInfoQueryOptions.getUserInfo(token), + ...queryOptions, + }) +} + +export const useUserInfoSuspenseQuery = ( + token: Tokens, + queryOptions: CustomQueryOptions = {}, +) => { + return useSuspenseQuery({ + ...userInfoQueryOptions.getUserInfo(token), + ...queryOptions, + }) +} diff --git a/packages/api/src/services/useUserInfoQuery/userInfoService.ts b/packages/api/src/services/useUserInfoQuery/userInfoService.ts new file mode 100644 index 0000000..4c6a271 --- /dev/null +++ b/packages/api/src/services/useUserInfoQuery/userInfoService.ts @@ -0,0 +1,15 @@ +import { baseFetcher } from '../..' +import { Tokens } from '../../shared/type' + +import { UserInfoResponse } from './model' + +export const userInfoService = { + async getUserInfo(tokens: Tokens) { + return baseFetcher.get('/user/info', { + headers: { + Authorization: tokens.access, + 'X-Refresh-Authorization': tokens.refresh, + }, + }) + }, +} diff --git a/packages/api/src/shared/type.ts b/packages/api/src/shared/type.ts index 970c64b..5ce23f7 100644 --- a/packages/api/src/shared/type.ts +++ b/packages/api/src/shared/type.ts @@ -1,6 +1,16 @@ -import { QueryOptions } from '@tanstack/react-query' +import { DefaultedQueryObserverOptions } from '@tanstack/react-query' export type CustomQueryOptions = Omit< - QueryOptions, - 'queryFn' | 'queryKey' + DefaultedQueryObserverOptions, + 'queryFn' | 'queryKey' | 'throwOnError' | 'refetchOnReconnect' | 'queryHash' > + +export interface DefaultResponse { + code: string + result: T +} + +export interface Tokens { + access: string + refresh: string +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 6a6d2f3..19260d1 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -37,7 +37,7 @@ "scripts": { "test": "vitest -c ./vitest.config.mts", "lint": "eslint . --max-warnings 0", - "dev": "vite build && vite build --mode ve --watch", + "dev": "vite build --watch ", "build": "vite build && vite build --mode ve" }, "peerDependencies": { @@ -49,12 +49,14 @@ "@storybook/test": "^8.0.10", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^15.0.5", + "@testing-library/user-event": "^14.5.2", "@turbo/gen": "^1.12.4", "@vanilla-extract/css": "^1.15.1", "@vanilla-extract/css-utils": "^0.1.3", "@vanilla-extract/recipes": "^0.5.2", "@vanilla-extract/sprinkles": "^1.6.1", "@vanilla-extract/vite-plugin": "^4.0.9", + "@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/ui": "^1.6.0", "@vook-client/eslint-config": "*", diff --git a/packages/design-system/src/components/Button/Button.css.ts b/packages/design-system/src/components/Button/Button.css.ts index 19051b6..ba23b78 100644 --- a/packages/design-system/src/components/Button/Button.css.ts +++ b/packages/design-system/src/components/Button/Button.css.ts @@ -11,6 +11,7 @@ export const button = recipe({ border: 'none', borderRadius: 6, gap: 6, + textDecoration: 'none', }, variants: { size: { diff --git a/packages/design-system/src/components/Button/Button.spec.tsx b/packages/design-system/src/components/Button/Button.spec.tsx index 1ff2b71..b4ee164 100644 --- a/packages/design-system/src/components/Button/Button.spec.tsx +++ b/packages/design-system/src/components/Button/Button.spec.tsx @@ -46,18 +46,18 @@ describe('Button Test', () => { it('Button은 prefixIcon이 있을 때 정상적으로 렌더링된다.', () => { // given - const { getAllByTitle } = render() + const { getByTitle } = render() // when & then - expect(getAllByTitle('X')[0]).toBeInTheDocument() + expect(getByTitle('X')).toBeInTheDocument() }) it('Button은 suffixIcon이 있을 때 정상적으로 렌더링된다.', () => { // given - const { getAllByTitle } = render() + const { getByTitle } = render() // when & then - expect(getAllByTitle('X')[1]).toBeInTheDocument() + expect(getByTitle('X')).toBeInTheDocument() }) it('Button은 prefixIcon과 suffixIcon이 모두 있을 때 정상적으로 렌더링된다.', () => { diff --git a/packages/design-system/src/components/Button/Button.tsx b/packages/design-system/src/components/Button/Button.tsx index d5655ea..c6a9a62 100644 --- a/packages/design-system/src/components/Button/Button.tsx +++ b/packages/design-system/src/components/Button/Button.tsx @@ -1,9 +1,9 @@ -import { ButtonHTMLAttributes, PropsWithChildren } from 'react' +import { ButtonHTMLAttributes, PropsWithChildren, forwardRef } from 'react' import { Text, TextProps } from '../Text' import { Icon, IconProps } from '../Icon' -import { ButtonVariants, blankIcon, button } from './Button.css' +import { ButtonVariants, button } from './Button.css' export type ButtonProps = ButtonHTMLAttributes & PropsWithChildren & @@ -21,47 +21,41 @@ const ButtonLabelType: { mini: 'label', } -export const Button = ({ - filled = true, - size = 'large', - blueLine = true, - disabled = false, - fit = 'hug', - prefixIcon, - suffixIcon, - name, - children, - ...rest -}: ButtonProps) => { - const textType = ButtonLabelType[size] - const fontWeight: TextProps['fontWeight'] = - size === 'mini' ? 'medium' : 'bold' +export const Button = forwardRef( + (props, ref) => { + const { + filled = true, + size = 'large', + blueLine = true, + disabled = false, + fit = 'hug', + prefixIcon, + suffixIcon, + name, + children, + ...rest + } = props - const onlySuffixIcon = prefixIcon === undefined && suffixIcon !== undefined - const onlyPrefixIcon = prefixIcon !== undefined && suffixIcon === undefined + const textType = ButtonLabelType[size] + const fontWeight: TextProps['fontWeight'] = + size === 'mini' ? 'medium' : 'bold' - return ( - - ) -} + return ( + + ) + }, +) + +Button.displayName = 'Button' diff --git a/packages/design-system/src/components/Checkbox/Checkbox.css.ts b/packages/design-system/src/components/Checkbox/Checkbox.css.ts new file mode 100644 index 0000000..9019bc5 --- /dev/null +++ b/packages/design-system/src/components/Checkbox/Checkbox.css.ts @@ -0,0 +1,43 @@ +import { style } from '@vanilla-extract/css' + +import { vars } from '../../styles/global.css' + +export const checkboxContainer = style({ + position: 'relative', + width: 22, + height: 22, +}) + +export const checkboxOutline = style({ + width: '100%', + height: '100%', + borderColor: vars.colors['semantic-line-normal'], + borderWidth: 1, + borderStyle: 'solid', + borderRadius: 3, + overflow: 'hidden', +}) + +export const checkedBox = style({ + width: '100%', + height: '100%', + backgroundColor: vars.colors['semantic-primary-normal'], +}) + +export const checkIcon = style({ + position: 'absolute', + width: 10, + height: 10, + top: '55%', + left: '55%', + transform: 'translate(-50%, -50%)', +}) + +export const realCheckboxInput = style({ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + opacity: 0, +}) diff --git a/packages/design-system/src/components/Checkbox/Checkbox.spec.tsx b/packages/design-system/src/components/Checkbox/Checkbox.spec.tsx new file mode 100644 index 0000000..b20c01a --- /dev/null +++ b/packages/design-system/src/components/Checkbox/Checkbox.spec.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { Checkbox } from './Checkbox' + +describe('Checkbox test', () => { + it('Checkbox은 정상적으로 렌더링된다.', () => { + // given + render( {}} />) + + // when + const checkbox = screen.getByRole('checkbox') + + // then + expect(checkbox).toBeInTheDocument() + }) + + it('Checkbox를 클릭할시 check 처리가 이루어진다.', async () => { + // given + render( {}} />) + const user = userEvent.setup() + + // when + const checkbox = screen.getByRole('checkbox') + await user.click(checkbox) + + // then + expect(checkbox).toBeChecked() + }) +}) diff --git a/packages/design-system/src/components/Checkbox/Checkbox.stories.tsx b/packages/design-system/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000..0f6960c --- /dev/null +++ b/packages/design-system/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Checkbox } from './Checkbox' + +const meta = { + title: 'Checkbox', + component: Checkbox, + args: { + onChange: () => {}, + checked: true, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Preview: Story = { + argTypes: { + onChange: { + table: { + disable: true, + }, + }, + checked: { + table: { + disable: true, + }, + }, + }, +} diff --git a/packages/design-system/src/components/Checkbox/Checkbox.tsx b/packages/design-system/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..401a5da --- /dev/null +++ b/packages/design-system/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,74 @@ +import { + ChangeEventHandler, + InputHTMLAttributes, + forwardRef, + useLayoutEffect, + useState, +} from 'react' + +import { + checkIcon, + checkboxContainer, + checkboxOutline, + checkedBox, + realCheckboxInput, +} from './Checkbox.css' + +export interface CheckboxProps + extends Omit, 'type'> { + onChange: ChangeEventHandler +} + +const CheckIcon = ( + + + +) + +export const Checkbox = forwardRef( + (props, ref) => { + const { onChange, ...rest } = props + const [checked, setChecked] = useState( + rest.checked === undefined ? false : rest.checked, + ) + + useLayoutEffect(() => { + setChecked(rest.checked === undefined ? false : rest.checked) + }, [rest.checked]) + + return ( +
+
+ {checked &&
} + {checked && CheckIcon} +
+ { + setChecked((prev) => !prev) + onChange(e) + }} + className={realCheckboxInput} + ref={ref} + type="checkbox" + checked={checked} + {...rest} + /> +
+ ) + }, +) + +Checkbox.displayName = 'Checkbox' diff --git a/packages/design-system/src/components/Checkbox/index.ts b/packages/design-system/src/components/Checkbox/index.ts new file mode 100644 index 0000000..ff4dd22 --- /dev/null +++ b/packages/design-system/src/components/Checkbox/index.ts @@ -0,0 +1 @@ +export { Checkbox } from './Checkbox' diff --git a/packages/design-system/src/components/Icon/Icon.stories.tsx b/packages/design-system/src/components/Icon/Icon.stories.tsx index 9109e9c..e706a49 100644 --- a/packages/design-system/src/components/Icon/Icon.stories.tsx +++ b/packages/design-system/src/components/Icon/Icon.stories.tsx @@ -124,6 +124,12 @@ export const Icons: Story = { {props.children} {props.children} +

Spinner

+ + {props.children} + {props.children} + {props.children} +

sns

{props.children} diff --git a/packages/design-system/src/components/Icon/icons/Spinner.tsx b/packages/design-system/src/components/Icon/icons/Spinner.tsx new file mode 100644 index 0000000..4f6f453 --- /dev/null +++ b/packages/design-system/src/components/Icon/icons/Spinner.tsx @@ -0,0 +1,166 @@ +/* eslint-disable react/no-unknown-property */ +import { icon } from '../Icon.css' + +export const spinnerIcons = { + 'spinner-big': ( + + + + + + + + + + + + + + {' '} + + ), + 'spinner-medium': ( + + + + + + + + + + + + + + + + ), + 'spinner-small': ( + + + + + + + + + + + + + + + + ), +} diff --git a/packages/design-system/src/components/Icon/icons/index.tsx b/packages/design-system/src/components/Icon/icons/index.tsx index 2aa8016..c821f9e 100644 --- a/packages/design-system/src/components/Icon/icons/index.tsx +++ b/packages/design-system/src/components/Icon/icons/index.tsx @@ -7,6 +7,7 @@ import { plusIcons } from './Plus' import { backwardIcons } from './Backward' import { snsIcons } from './SNS' import { emogiIcons } from './Emoji' +import { spinnerIcons } from './Spinner' export type IconNames = | keyof typeof closeIcons @@ -18,6 +19,7 @@ export type IconNames = | keyof typeof backwardIcons | keyof typeof snsIcons | keyof typeof emogiIcons + | keyof typeof spinnerIcons export const Icons: { [key in IconNames]: JSX.Element @@ -31,6 +33,7 @@ export const Icons: { ...backwardIcons, ...snsIcons, ...emogiIcons, + ...spinnerIcons, } export const iconNames = Object.keys(Icons) as IconNames[] diff --git a/packages/design-system/src/components/Input/Input.css.ts b/packages/design-system/src/components/Input/Input.css.ts new file mode 100644 index 0000000..fac19f8 --- /dev/null +++ b/packages/design-system/src/components/Input/Input.css.ts @@ -0,0 +1,75 @@ +import { style } from '@vanilla-extract/css' +import { recipe, RecipeVariants } from '@vanilla-extract/recipes' + +import { vars } from '../../styles/global.css' + +export const inputLabel = style({ + display: 'flex', + flexDirection: 'column', + gap: 8, +}) + +export const invalidInputLabel = style({ + position: 'absolute', +}) + +export const requirementText = style({ + position: 'absolute', + left: 0, + bottom: -24, + color: vars.colors['status-error'], +}) + +export const input = recipe({ + base: { + boxSizing: 'border-box', + width: '100%', + height: 48, + padding: '14px 16px', + + borderRadius: '5px', + + fontSize: 14, + + ':disabled': { + backgroundColor: vars.colors['component-alternative'], + }, + + ':focus': { + color: vars.colors['semantic-label-normal'], + outline: 'none', + }, + + '::placeholder': { + color: vars.colors['semantic-label-assistive'], + }, + }, + + variants: { + invalid: { + false: { + border: `1px solid ${vars.colors['semantic-line-normal']}`, + }, + true: { + border: `1px solid ${vars.colors['status-error']}`, + }, + }, + }, + + defaultVariants: { + invalid: false, + }, +}) + +export const inputIcon = style({ + position: 'absolute', + top: '50%', + left: 16, + transform: 'translateY(-50%)', +}) + +export const withIcon = style({ + paddingLeft: 44, +}) + +export type InputVariants = RecipeVariants diff --git a/packages/design-system/src/components/Input/Input.spec.tsx b/packages/design-system/src/components/Input/Input.spec.tsx new file mode 100644 index 0000000..bec06d7 --- /dev/null +++ b/packages/design-system/src/components/Input/Input.spec.tsx @@ -0,0 +1,97 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { Input } from './Input' + +describe('Input test', () => { + it('Input은 정상적으로 렌더링된다.', () => { + // given + render() + + // when + const input = screen.getByRole('textbox') + + // then + expect(input).toBeInTheDocument() + }) + + it('Input은 label을 렌더링한다.', () => { + // given + render() + + // when + const label = screen.getByText('label') + + // then + expect(label).toBeInTheDocument() + }) + it('Input은 placeHolder를 렌더링한다.', () => { + // given + render() + + // when + const input = screen.getByPlaceholderText('placeholder') + + // then + expect(input).toBeInTheDocument() + }) + + it('Input은 disable이 true일시 비활성화된다.', () => { + // given + render() + + // when + const input = screen.getByRole('textbox') + + // then + expect(input).toBeDisabled() + }) + + it('Input에 타이핑할시 value가 변경된다.', async () => { + // given + render() + const user = userEvent.setup() + + // when + const input = screen.getByRole('textbox') + await user.type(input, 'value') + + // then + expect(input).toHaveValue('value') + }) + + it('Input은 required가 true일시 필수 입력요소가 된다', () => { + // given + render() + + // when + const input = screen.getByRole('textbox') + + // then + expect(input).toBeRequired() + }) + + it('Input은 readOnly가 true일시 타이핑해도 value가 변경되지 않는다.', async () => { + // given + render() + const user = userEvent.setup() + + // when + const input = screen.getByRole('textbox') + await user.type(input, 'value') + + // then + expect(input).toHaveValue('') + }) + + it('Input은 invalid가 true일시 requirement를 출력한다.', () => { + // given + render() + + // when + const requirement = screen.getByText('3자 이상 입력해주세요!') + + // then + expect(requirement).toBeInTheDocument() + }) +}) diff --git a/packages/design-system/src/components/Input/Input.stories.tsx b/packages/design-system/src/components/Input/Input.stories.tsx new file mode 100644 index 0000000..cb76645 --- /dev/null +++ b/packages/design-system/src/components/Input/Input.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { iconNames } from '../Icon/icons' + +import { Input } from './Input' + +const meta = { + title: 'Input', + component: Input, + args: { + label: 'Label', + placeholder: 'Typing', + disabled: false, + invalid: false, + requirement: 'Error', + }, + argTypes: { + label: { + control: 'text', + description: 'input 라벨', + }, + placeholder: { + control: 'text', + description: 'input placeholder', + }, + disabled: { + control: 'boolean', + description: 'input 비활성화 여부', + }, + invalid: { + invalid: 'boolean', + description: 'input 유효성 확인 상태', + }, + requirement: { + invalid: 'text', + description: 'input 유효성 확인 문구', + }, + icon: { + options: iconNames, + control: { type: 'select' }, + description: 'input 아이콘', + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = {} diff --git a/packages/design-system/src/components/Input/Input.tsx b/packages/design-system/src/components/Input/Input.tsx new file mode 100644 index 0000000..bbdee83 --- /dev/null +++ b/packages/design-system/src/components/Input/Input.tsx @@ -0,0 +1,88 @@ +'use client' + +import { InputHTMLAttributes, forwardRef } from 'react' +import clsx from 'clsx' + +import { Text } from '../Text' +import { Icon } from '../Icon' +import { sprinkles } from '../../styles/sprinkles.css' +import { IconNames } from '../Icon/icons' + +import { + InputVariants, + input, + inputIcon, + inputLabel, + requirementText, + withIcon, +} from './Input.css' + +export type InputProps = { + label?: string + invalid?: boolean + icon?: IconNames + readonly?: boolean + requirement?: string +} & InputVariants & + InputHTMLAttributes + +export const Input = forwardRef((props, ref) => { + const { + label, + width, + invalid = false, + requirement = '', + readOnly = false, + icon, + ...rest + } = props + + const inputElement = ( + + + {icon && ( +
+ +
+ )} + {invalid && ( + + {requirement} + + )} +
+ ) + + if (label) { + return ( + + ) + } + + return {inputElement} +}) + +Input.displayName = 'Input' diff --git a/packages/design-system/src/components/Input/index.ts b/packages/design-system/src/components/Input/index.ts new file mode 100644 index 0000000..ed62e15 --- /dev/null +++ b/packages/design-system/src/components/Input/index.ts @@ -0,0 +1,2 @@ +export type { InputProps } from './Input' +export { Input } from './Input' diff --git a/packages/design-system/src/components/SelectBox/SelectBox.css.ts b/packages/design-system/src/components/SelectBox/SelectBox.css.ts new file mode 100644 index 0000000..1751e4e --- /dev/null +++ b/packages/design-system/src/components/SelectBox/SelectBox.css.ts @@ -0,0 +1,64 @@ +import { recipe, RecipeVariants } from '@vanilla-extract/recipes' +import { style } from '@vanilla-extract/css' + +import { vars } from '../../styles/global.css' + +export const selectBox = recipe({ + base: { + position: 'relative', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + padding: '12px 16px', + + border: `1px solid ${vars.colors['semantic-line-normal']}`, + borderRadius: 8, + gap: 6, + }, + variants: { + fit: { + default: { + width: 180, + }, + fill: { + width: '100%', + }, + hug: { + width: 'fit-content', + }, + }, + selected: { + true: { + border: `1px solid ${vars.colors['semantic-primary-normal']}`, + backgroundColor: vars.colors['palette-primary-50'], + }, + false: { + ':hover': { + cursor: 'pointer', + backgroundColor: vars.colors['component-normal'], + }, + }, + }, + }, + defaultVariants: { + fit: 'default', + selected: false, + }, +}) + +export const fakeSelectBox = style({ + position: 'absolute', + display: 'block', + width: '100%', + height: '100%', + opacity: 0, + margin: 0, + + ':hover': { + cursor: 'pointer', + }, + + transform: 'translate(-54px, -38px)', +}) + +export type SelectBoxVariants = RecipeVariants diff --git a/packages/design-system/src/components/SelectBox/SelectBox.spec.tsx b/packages/design-system/src/components/SelectBox/SelectBox.spec.tsx new file mode 100644 index 0000000..0363a7c --- /dev/null +++ b/packages/design-system/src/components/SelectBox/SelectBox.spec.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' + +import { SelectBox } from './SelectBox' + +describe('SelectBox test', () => { + it('SelectBox는 정상적으로 렌더링된다.', () => { + // given + render( {}} />) + + // when + const selectBox = screen.getByRole('checkbox') + + // then + expect(selectBox).toBeInTheDocument() + }) + + it('SelectBox를 클릭할시 check 처리가 이루어진다.', async () => { + // given + render( {}} />) + const user = userEvent.setup() + + // when + const selectBox = screen.getByRole('checkbox') + await user.click(selectBox) + + // then + expect(selectBox).toBeChecked() + }) +}) diff --git a/packages/design-system/src/components/SelectBox/SelectBox.stories.tsx b/packages/design-system/src/components/SelectBox/SelectBox.stories.tsx new file mode 100644 index 0000000..4d76370 --- /dev/null +++ b/packages/design-system/src/components/SelectBox/SelectBox.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { iconNames } from '../Icon/icons' + +import { SelectBox } from './SelectBox' + +const meta = { + title: 'SelectBox', + component: SelectBox, + tags: ['autodocs'], + args: { + children: '엑스', + prefixIcon: 'X', + selected: false, + }, + argTypes: { + children: { control: 'text', description: '셀렉트 박스 텍스트' }, + prefixIcon: { + options: [...iconNames, null], + control: { type: 'select' }, + description: '셀렉트 박스 prefix 아이콘', + }, + suffixIcon: { + options: [...iconNames, null], + control: { type: 'select' }, + description: '셀렉트 박스 suffix 아이콘', + }, + fit: { + options: ['fill', 'default', 'hug'], + control: { type: 'select' }, + description: '셀렉트 박스 너비', + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = {} diff --git a/packages/design-system/src/components/SelectBox/SelectBox.tsx b/packages/design-system/src/components/SelectBox/SelectBox.tsx new file mode 100644 index 0000000..35d06d0 --- /dev/null +++ b/packages/design-system/src/components/SelectBox/SelectBox.tsx @@ -0,0 +1,61 @@ +import { + HTMLAttributes, + PropsWithChildren, + forwardRef, + useLayoutEffect, + useState, +} from 'react' + +import { Text } from '../Text' +import { Icon, IconProps } from '../Icon' + +import { SelectBoxVariants, fakeSelectBox, selectBox } from './SelectBox.css' + +export type SelectBoxProps = HTMLAttributes & + PropsWithChildren & + SelectBoxVariants & { + prefixIcon?: Exclude + suffixIcon?: Exclude + } + +export const SelectBox = forwardRef( + (props, ref) => { + const { fit, prefixIcon, suffixIcon, children, ...rest } = props + + const [selected, setSelected] = useState( + rest.selected === undefined ? false : rest.selected, + ) + + useLayoutEffect(() => { + setSelected(rest.selected === undefined ? false : rest.selected) + }, [rest.selected]) + + return ( +
+ {prefixIcon && } + +
+ ) + }, +) + +SelectBox.displayName = 'SelectBox' diff --git a/packages/design-system/src/components/SelectBox/index.ts b/packages/design-system/src/components/SelectBox/index.ts new file mode 100644 index 0000000..33a9579 --- /dev/null +++ b/packages/design-system/src/components/SelectBox/index.ts @@ -0,0 +1 @@ +export { SelectBox } from './SelectBox' diff --git a/packages/design-system/src/components/Step/Step.css.ts b/packages/design-system/src/components/Step/Step.css.ts new file mode 100644 index 0000000..ac5b9cf --- /dev/null +++ b/packages/design-system/src/components/Step/Step.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css' + +import { vars } from '../../styles/global.css' + +export const stepContainer = style({ + display: 'flex', + gap: 6, +}) + +export const step = style({ + width: 20, + height: 4, + + backgroundColor: vars.colors['palette-primary-50'], + borderRadius: 2, + + selectors: { + '&.completed': { + backgroundColor: vars.colors['semantic-primary-normal'], + }, + }, +}) + +export const completedStep = style({ + color: 'green', +}) diff --git a/packages/design-system/src/components/Step/Step.spec.tsx b/packages/design-system/src/components/Step/Step.spec.tsx new file mode 100644 index 0000000..3bffd2a --- /dev/null +++ b/packages/design-system/src/components/Step/Step.spec.tsx @@ -0,0 +1,6 @@ +describe('Step Logo', () => { + it('Step은 정상적으로 렌더링 된다.', () => { + // const { getByTestId } = render() + // expect(getByTitle('symbol logo')).toBeInTheDocument() + }) +}) diff --git a/packages/design-system/src/components/Step/Step.stories.tsx b/packages/design-system/src/components/Step/Step.stories.tsx new file mode 100644 index 0000000..8b33f06 --- /dev/null +++ b/packages/design-system/src/components/Step/Step.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react' + +import { Step } from './Step' + +const meta = { + title: 'Step', + component: Step, + tags: ['autodocs'], + args: { + current: 1, + total: 10, + }, + argTypes: { + current: { + type: 'number', + description: '현재 스텝', + }, + total: { + type: 'number', + description: '총 스텝', + }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = {} diff --git a/packages/design-system/src/components/Step/Step.tsx b/packages/design-system/src/components/Step/Step.tsx new file mode 100644 index 0000000..b565fce --- /dev/null +++ b/packages/design-system/src/components/Step/Step.tsx @@ -0,0 +1,24 @@ +import clsx from 'clsx' + +import { step, stepContainer } from './Step.css' + +export interface StepProps { + current: number + total: number +} + +export const Step = ({ current, total }: StepProps) => { + return ( +
+ {Array.from({ length: total }).map((_, index) => ( +
+ ))} +
+ ) +} diff --git a/packages/design-system/src/components/Step/index.ts b/packages/design-system/src/components/Step/index.ts new file mode 100644 index 0000000..6278934 --- /dev/null +++ b/packages/design-system/src/components/Step/index.ts @@ -0,0 +1 @@ +export { Step } from './Step' diff --git a/packages/design-system/src/components/Text/Text.css.ts b/packages/design-system/src/components/Text/Text.css.ts index 5f8c121..ef16510 100644 --- a/packages/design-system/src/components/Text/Text.css.ts +++ b/packages/design-system/src/components/Text/Text.css.ts @@ -4,9 +4,10 @@ import { fontWeights } from '../../tokens/typography' export const text = recipe({ base: { - display: 'inline', + margin: 0, letterSpacing: '0.01em', whiteSpace: 'pre-wrap', + textDecoration: 'none', }, variants: { type: { diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index d56e573..62bfc70 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -1,4 +1,5 @@ /* eslint-disable simple-import-sort/exports */ +'use client' export type { Tokens } from './tokens' export { tokens } from './tokens' @@ -8,10 +9,15 @@ export type { ButtonProps } from './components/Button' export { Button } from './components/Button' export type { ListProps } from './components/List' export { List } from './components/List' -export type { TextProps } from './components/Text' export { Text } from './components/Text' +export type { TextProps } from './components/Text' export { SymbolLogo } from './components/SymbolLogo' export { TypoLogo } from './components/TypoLogo' +export { Input } from './components/Input' +export type { InputProps } from './components/Input' +export { Step } from './components/Step' +export { Checkbox } from './components/Checkbox' +export { SelectBox } from './components/SelectBox' export type { Sprinkles } from './styles/sprinkles.css' export { sprinkles } from './styles/sprinkles.css' export { vars } from './styles/vars' diff --git a/packages/design-system/src/tokens/colors.ts b/packages/design-system/src/tokens/colors.ts index 3a00d4a..64223a1 100644 --- a/packages/design-system/src/tokens/colors.ts +++ b/packages/design-system/src/tokens/colors.ts @@ -58,6 +58,15 @@ const hiliting = { yellow: '#FFF2B2', } +const label = { + 'label-neutral': 'rgba(22, 23, 25, 0.88)', + 'label-alternative': 'rgba(22, 23, 25, 0.6)', +} + +const status = { + 'status-error': '#FF3333', +} + export const colors = { ...semantic, ...link, @@ -65,5 +74,7 @@ export const colors = { ...palette, ...component, ...hiliting, + ...label, + ...status, inherit: 'inherit', } as const diff --git a/packages/design-system/vite.config.ts b/packages/design-system/vite.config.ts index dcd8fe9..652819f 100644 --- a/packages/design-system/vite.config.ts +++ b/packages/design-system/vite.config.ts @@ -60,6 +60,7 @@ const bundle = defineConfig({ rollupOptions: { output: { dir: 'dist/bundle/', + banner: '"use client"', }, external: shared.external, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6362532..6b3b1fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,29 +6,10 @@ settings: importers: .: - dependencies: - '@testing-library/user-event': - specifier: ^14.5.2 - version: 14.5.2(@testing-library/dom@10.1.0) devDependencies: - '@testing-library/jest-dom': - specifier: ^6.4.2 - version: 6.4.5(vitest@1.6.0) - '@testing-library/react': - specifier: ^15.0.5 - version: 15.0.7(react-dom@18.3.1)(react@18.3.1) '@titicaca/prettier-config-triple': specifier: ^1.1.0 version: 1.1.0(prettier@3.2.5) - '@vanilla-extract/vite-plugin': - specifier: ^4.0.9 - version: 4.0.9(@types/node@20.12.12)(vite@5.2.11) - '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.2.1(vite@5.2.11) - '@vitest/ui': - specifier: ^1.6.0 - version: 1.6.0(vitest@1.6.0) '@vook-client/eslint-config': specifier: '*' version: link:packages/eslint-config @@ -37,34 +18,22 @@ importers: version: link:packages/typescript-config eslint-plugin-vitest: specifier: ^0.2.6 - version: 0.2.8(eslint@8.57.0)(typescript@5.2.2)(vite@5.2.11)(vitest@1.6.0) + version: 0.2.8(eslint@8.57.0)(typescript@5.2.2)(vitest@1.6.0) husky: specifier: ^9.0.11 version: 9.0.11 - jsdom: - specifier: ^24.0.0 - version: 24.0.0 lint-staged: specifier: ^15.2.2 version: 15.2.2 prettier: specifier: ^3.2.5 version: 3.2.5 - react: - specifier: ^18.3.1 - version: 18.3.1 turbo: specifier: latest version: 1.13.3 typescript: specifier: 5.2.2 version: 5.2.2 - vite-tsconfig-paths: - specifier: ^4.3.2 - version: 4.3.2(typescript@5.2.2)(vite@5.2.11) - vitest: - specifier: ^1.5.2 - version: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) apps/extension: dependencies: @@ -123,6 +92,9 @@ importers: '@types/react-dom': specifier: ^18.2.19 version: 18.3.0 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.2.11) '@vitest/ui': specifier: ^1.6.0 version: 1.6.0(vitest@1.6.0) @@ -140,13 +112,19 @@ importers: version: 5.2.2 vitest: specifier: ^1.5.2 - version: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) + version: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0) apps/web: dependencies: + '@hookform/resolvers': + specifier: ^3.6.0 + version: 3.6.0(react-hook-form@7.51.5) '@tanstack/react-query': specifier: ^5.32.0 version: 5.37.1(react@18.3.1) + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@vanilla-extract/css': specifier: ^1.14.2 version: 1.15.1 @@ -168,15 +146,27 @@ importers: '@vook-client/design-system': specifier: '*' version: link:../../packages/design-system + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 next: specifier: ^14.1.1 - version: 14.2.3(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1) + version: 14.2.3(@babel/core@7.23.7)(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.51.5 + version: 7.51.5(react@18.3.1) + vite-tsconfig-paths: + specifier: ^4.3.2 + version: 4.3.2(typescript@5.2.2)(vite@5.2.11) + zod: + specifier: ^3.23.8 + version: 3.23.8 zustand: specifier: ^4.5.2 version: 4.5.2(@types/react@18.3.2)(react@18.3.1) @@ -193,6 +183,15 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.32.0 version: 5.37.1(@tanstack/react-query@5.37.1)(react@18.3.1) + '@testing-library/jest-dom': + specifier: ^6.4.2 + version: 6.4.5(vitest@1.6.0) + '@testing-library/react': + specifier: ^15.0.5 + version: 15.0.7(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.1.0) '@types/eslint': specifier: ^8.56.5 version: 8.56.10 @@ -208,9 +207,15 @@ importers: '@vanilla-extract/next-plugin': specifier: ^2.4.0 version: 2.4.0(@types/node@20.12.12)(next@14.2.3)(webpack@5.91.0) + '@vanilla-extract/vite-plugin': + specifier: ^4.0.9 + version: 4.0.9(@types/node@20.12.12)(vite@5.2.11) '@vanilla-extract/webpack-plugin': specifier: ^2.3.7 version: 2.3.8(@types/node@20.12.12)(webpack@5.91.0) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.2.11) '@vook-client/eslint-config': specifier: '*' version: link:../../packages/eslint-config @@ -220,21 +225,42 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 eslint: specifier: ^8.57.0 version: 8.57.0 + jsdom: + specifier: ^24.0.0 + version: 24.0.0 + msw: + specifier: ^2.3.1 + version: 2.3.1(typescript@5.2.2) + vitest: + specifier: ^1.5.2 + version: 1.6.0(@types/node@20.12.12)(jsdom@24.0.0) apps/workshop: dependencies: + '@tanstack/react-query': + specifier: ^5.32.0 + version: 5.37.1(react@18.3.1) + '@vook-client/api': + specifier: '*' + version: link:../../packages/api + '@vook-client/design-system': + specifier: '*' + version: link:../../packages/design-system + next: + specifier: ^14.1.1 + version: 14.2.3(@babel/core@7.23.7)(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - ui: - specifier: '*' - version: 0.2.4 devDependencies: '@chromatic-com/storybook': specifier: ^1.3.3 @@ -257,6 +283,9 @@ importers: '@storybook/blocks': specifier: ^8.0.10 version: 8.1.1(@types/react-dom@18.3.0)(@types/react@18.3.2)(prettier@3.2.5)(react-dom@18.3.1)(react@18.3.1) + '@storybook/manager-api': + specifier: ^8.1.7 + version: 8.1.7(react-dom@18.3.1)(react@18.3.1) '@storybook/react': specifier: ^8.0.10 version: 8.1.1(prettier@3.2.5)(react-dom@18.3.1)(react@18.3.1)(typescript@5.2.2) @@ -266,6 +295,9 @@ importers: '@storybook/test': specifier: ^8.0.10 version: 8.1.1(vitest@1.6.0) + '@storybook/theming': + specifier: ^8.1.7 + version: 8.1.7(react-dom@18.3.1)(react@18.3.1) '@types/react': specifier: ^18.2.15 version: 18.3.2 @@ -277,7 +309,7 @@ importers: version: 4.0.9(vite@4.5.3) '@vanilla-extract/webpack-plugin': specifier: ^2.3.7 - version: 2.3.8(@types/node@20.12.12)(webpack@5.91.0) + version: 2.3.8(webpack@5.91.0) '@vitejs/plugin-react': specifier: ^4.0.3 version: 4.2.1(vite@4.5.3) @@ -293,6 +325,12 @@ importers: mini-css-extract-plugin: specifier: ^2.9.0 version: 2.9.0(webpack@5.91.0) + msw: + specifier: ^2.3.1 + version: 2.3.1(typescript@5.2.2) + msw-storybook-addon: + specifier: ^2.0.2 + version: 2.0.2(msw@2.3.1) storybook: specifier: ^8.0.10 version: 8.1.1(react-dom@18.3.1)(react@18.3.1) @@ -302,12 +340,21 @@ importers: vite: specifier: ^4.4.5 version: 4.5.3 + vite-tsconfig-paths: + specifier: ^4.3.2 + version: 4.3.2(typescript@5.2.2)(vite@4.5.3) packages/api: dependencies: '@tanstack/react-query': specifier: ^5.32.0 version: 5.37.1(react@18.3.1) + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 devDependencies: '@vook-client/eslint-config': specifier: '*' @@ -315,6 +362,12 @@ importers: '@vook-client/typescript-config': specifier: '*' version: link:../typescript-config + msw: + specifier: ^2.3.1 + version: 2.3.1(typescript@5.2.2) + zod: + specifier: ^3.23.8 + version: 3.23.8 packages/design-system: dependencies: @@ -336,10 +389,13 @@ importers: version: 6.4.5(vitest@1.6.0) '@testing-library/react': specifier: ^15.0.5 - version: 15.0.7(react-dom@18.3.1)(react@18.3.1) + version: 15.0.7(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.1.0) '@turbo/gen': specifier: ^1.12.4 - version: 1.13.3(@types/node@20.12.12)(typescript@5.2.2) + version: 1.13.3(@types/node@20.14.2)(typescript@5.2.2) '@vanilla-extract/css': specifier: ^1.15.1 version: 1.15.1 @@ -354,7 +410,10 @@ importers: version: 1.6.1(@vanilla-extract/css@1.15.1) '@vanilla-extract/vite-plugin': specifier: ^4.0.9 - version: 4.0.9(@types/node@20.12.12)(vite@5.2.11) + version: 4.0.9(@types/node@20.14.2)(vite@5.2.11) + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.3.1(vite@5.2.11) '@vitejs/plugin-react-swc': specifier: ^3.5.0 version: 3.7.0(vite@5.2.11) @@ -387,13 +446,13 @@ importers: version: 5.2.2 vite: specifier: ^5.2.0 - version: 5.2.11(@types/node@20.12.12) + version: 5.2.11(@types/node@20.14.2) vite-plugin-dts: specifier: ^3.9.1 - version: 3.9.1(@types/node@20.12.12)(typescript@5.2.2)(vite@5.2.11) + version: 3.9.1(@types/node@20.14.2)(typescript@5.2.2)(vite@5.2.11) vitest: specifier: ^1.5.2 - version: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) + version: 1.6.0(@types/node@20.14.2)(@vitest/ui@1.6.0) packages/eslint-config: devDependencies: @@ -518,6 +577,7 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true /@babel/generator@7.24.5: resolution: @@ -715,6 +775,7 @@ packages: '@babel/helper-simple-access': 7.24.5 '@babel/helper-split-export-declaration': 7.24.5 '@babel/helper-validator-identifier': 7.24.5 + dev: true /@babel/helper-optimise-call-expression@7.22.5: resolution: @@ -1868,6 +1929,19 @@ packages: '@babel/helper-plugin-utils': 7.24.5 dev: true + /@babel/plugin-transform-react-jsx-self@7.24.5(@babel/core@7.24.5): + resolution: + { + integrity: sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + dev: true + /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.23.7): resolution: { @@ -1881,6 +1955,19 @@ packages: '@babel/helper-plugin-utils': 7.24.5 dev: true + /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.24.5): + resolution: + { + integrity: sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-plugin-utils': 7.24.5 + dev: true + /@babel/plugin-transform-regenerator@7.24.1(@babel/core@7.24.5): resolution: { @@ -2279,6 +2366,24 @@ packages: } dev: true + /@bundled-es-modules/cookie@2.0.0: + resolution: + { + integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==, + } + dependencies: + cookie: 0.5.0 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: + { + integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==, + } + dependencies: + statuses: 2.0.1 + dev: true + /@chromatic-com/storybook@1.4.0(react@18.3.1): resolution: { @@ -3890,6 +3995,17 @@ packages: tslib: 2.6.2 dev: false + /@hookform/resolvers@3.6.0(react-hook-form@7.51.5): + resolution: + { + integrity: sha512-UBcpyOX3+RR+dNnqBd0lchXpoL8p4xC21XP8H6Meb8uve5Br1GCnmg0PcBoKKqPKgGu9GHQ/oygcmPrQhetwqw==, + } + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.51.5(react@18.3.1) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: { @@ -3919,6 +4035,55 @@ packages: } dev: true + /@inquirer/confirm@3.1.9: + resolution: + { + integrity: sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==, + } + engines: { node: '>=18' } + dependencies: + '@inquirer/core': 8.2.2 + '@inquirer/type': 1.3.3 + dev: true + + /@inquirer/core@8.2.2: + resolution: + { + integrity: sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==, + } + engines: { node: '>=18' } + dependencies: + '@inquirer/figures': 1.0.3 + '@inquirer/type': 1.3.3 + '@types/mute-stream': 0.0.4 + '@types/node': 20.14.2 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /@inquirer/figures@1.0.3: + resolution: + { + integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==, + } + engines: { node: '>=18' } + dev: true + + /@inquirer/type@1.3.3: + resolution: + { + integrity: sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==, + } + engines: { node: '>=18' } + dev: true + /@isaacs/cliui@8.0.2: resolution: { @@ -4211,7 +4376,7 @@ packages: react: 18.3.1 dev: true - /@microsoft/api-extractor-model@7.28.13(@types/node@20.12.12): + /@microsoft/api-extractor-model@7.28.13(@types/node@20.14.2): resolution: { integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==, @@ -4219,25 +4384,25 @@ packages: dependencies: '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.12) + '@rushstack/node-core-library': 4.0.2(@types/node@20.14.2) transitivePeerDependencies: - '@types/node' dev: true - /@microsoft/api-extractor@7.43.0(@types/node@20.12.12): + /@microsoft/api-extractor@7.43.0(@types/node@20.14.2): resolution: { integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==, } hasBin: true dependencies: - '@microsoft/api-extractor-model': 7.28.13(@types/node@20.12.12) + '@microsoft/api-extractor-model': 7.28.13(@types/node@20.14.2) '@microsoft/tsdoc': 0.14.2 '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.12) + '@rushstack/node-core-library': 4.0.2(@types/node@20.14.2) '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.0(@types/node@20.12.12) - '@rushstack/ts-command-line': 4.19.1(@types/node@20.12.12) + '@rushstack/terminal': 0.10.0(@types/node@20.14.2) + '@rushstack/ts-command-line': 4.19.1(@types/node@20.14.2) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -4357,6 +4522,29 @@ packages: dev: false optional: true + /@mswjs/cookies@1.1.0: + resolution: + { + integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==, + } + engines: { node: '>=18' } + dev: true + + /@mswjs/interceptors@0.29.1: + resolution: + { + integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==, + } + engines: { node: '>=18' } + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.2 + strict-event-emitter: 0.5.1 + dev: true + /@ndelangen/get-tarball@3.0.9: resolution: { @@ -4518,6 +4706,30 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + /@open-draft/deferred-promise@2.2.0: + resolution: + { + integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==, + } + dev: true + + /@open-draft/logger@0.3.0: + resolution: + { + integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==, + } + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + dev: true + + /@open-draft/until@2.1.0: + resolution: + { + integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==, + } + dev: true + /@parcel/bundler-default@2.9.3(@parcel/core@2.9.3): resolution: { @@ -6882,7 +7094,7 @@ packages: } dev: true - /@rushstack/node-core-library@4.0.2(@types/node@20.12.12): + /@rushstack/node-core-library@4.0.2(@types/node@20.14.2): resolution: { integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==, @@ -6893,7 +7105,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.12.12 + '@types/node': 20.14.2 fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 @@ -6912,7 +7124,7 @@ packages: strip-json-comments: 3.1.1 dev: true - /@rushstack/terminal@0.10.0(@types/node@20.12.12): + /@rushstack/terminal@0.10.0(@types/node@20.14.2): resolution: { integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==, @@ -6923,18 +7135,18 @@ packages: '@types/node': optional: true dependencies: - '@rushstack/node-core-library': 4.0.2(@types/node@20.12.12) - '@types/node': 20.12.12 + '@rushstack/node-core-library': 4.0.2(@types/node@20.14.2) + '@types/node': 20.14.2 supports-color: 8.1.1 dev: true - /@rushstack/ts-command-line@4.19.1(@types/node@20.12.12): + /@rushstack/ts-command-line@4.19.1(@types/node@20.14.2): resolution: { integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==, } dependencies: - '@rushstack/terminal': 0.10.0(@types/node@20.12.12) + '@rushstack/terminal': 0.10.0(@types/node@20.14.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -7308,6 +7520,19 @@ packages: tiny-invariant: 1.3.3 dev: true + /@storybook/channels@8.1.7: + resolution: + { + integrity: sha512-L1jrgaleNBTLNRH35iNxmIDWEqFhouDbq7Vii9FgjSOJdScUHVdtxzC8A2ymXlQCiD5ggQ5HzmUJaY6RTfwGRg==, + } + dependencies: + '@storybook/client-logger': 8.1.7 + '@storybook/core-events': 8.1.7 + '@storybook/global': 5.0.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + dev: true + /@storybook/cli@8.1.1(react-dom@18.3.1)(react@18.3.1): resolution: { @@ -7370,6 +7595,15 @@ packages: '@storybook/global': 5.0.0 dev: true + /@storybook/client-logger@8.1.7: + resolution: + { + integrity: sha512-Cmdt9qpyIQZcVR3y16464vrO06YFaWice+wQZ1OIror8XBqkpUxgZldQ95uTed6Wz9igf0PEYyaV8jJrGcHMrA==, + } + dependencies: + '@storybook/global': 5.0.0 + dev: true + /@storybook/codemod@8.1.1: resolution: { @@ -7477,6 +7711,16 @@ packages: ts-dedent: 2.2.0 dev: true + /@storybook/core-events@8.1.7: + resolution: + { + integrity: sha512-cASpI+C+S1DUiO7schq7jKwvEuFwkqR24PTQxe4o77DMiryCJZgw+YlUHXS8EodKJW5cLVB3wd3fHAYYfeyWGg==, + } + dependencies: + '@storybook/csf': 0.1.7 + ts-dedent: 2.2.0 + dev: true + /@storybook/core-server@8.1.1(prettier@3.2.5)(react-dom@18.3.1)(react@18.3.1): resolution: { @@ -7677,6 +7921,32 @@ packages: - react-dom dev: true + /@storybook/manager-api@8.1.7(react-dom@18.3.1)(react@18.3.1): + resolution: + { + integrity: sha512-sLVieFaDSd6Xrl4V/mgL2mq4Js8IjmeGknj0TZaAmN6Xbwq4+W0pRyyVuFNEG8SpdxwYk7BsbtkD9+tXYlLElw==, + } + dependencies: + '@storybook/channels': 8.1.7 + '@storybook/client-logger': 8.1.7 + '@storybook/core-events': 8.1.7 + '@storybook/csf': 0.1.7 + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@18.3.1)(react@18.3.1) + '@storybook/router': 8.1.7 + '@storybook/theming': 8.1.7(react-dom@18.3.1)(react@18.3.1) + '@storybook/types': 8.1.7 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.3 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + dev: true + /@storybook/manager@8.1.1: resolution: { @@ -7880,6 +8150,17 @@ packages: qs: 6.12.1 dev: true + /@storybook/router@8.1.7: + resolution: + { + integrity: sha512-1NjHXYV1bDn7qzhF8ZefMLJR/P2tOSU9+NhDzCSl0jxZGjFPJWpciVX5dheRNAOASNaUr5l3BxzFXo6Tv4jcsA==, + } + dependencies: + '@storybook/client-logger': 8.1.7 + memoizerific: 1.11.3 + qs: 6.12.1 + dev: true + /@storybook/telemetry@8.1.1(prettier@3.2.5): resolution: { @@ -7946,6 +8227,28 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: true + /@storybook/theming@8.1.7(react-dom@18.3.1)(react@18.3.1): + resolution: + { + integrity: sha512-iIg1+SBv3d9aCyHp7soPPglfn2GoP69Xp+F8nfdo8lx+SHaWxRCqvW+jiZaJur0c4yqKsFpDrvWjYa4xWfQP7w==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) + '@storybook/client-logger': 8.1.7 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: true + /@storybook/types@8.1.1: resolution: { @@ -7957,6 +8260,17 @@ packages: file-system-cache: 2.3.0 dev: true + /@storybook/types@8.1.7: + resolution: + { + integrity: sha512-OkdxFvqvRc6eCOMwLyx8zCTAox71PcEW+0BZgZGeL7uunF5pA615LFCU79LfwY/dUQNjbv9HhQu/feTu16GVvQ==, + } + dependencies: + '@storybook/channels': 8.1.7 + '@types/express': 4.17.21 + file-system-cache: 2.3.0 + dev: true + /@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.23.7): resolution: { @@ -8515,6 +8829,7 @@ packages: dom-accessibility-api: 0.5.16 lz-string: 1.5.0 pretty-format: 27.5.1 + dev: true /@testing-library/dom@9.3.4: resolution: @@ -8565,7 +8880,7 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) + vitest: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0) dev: true /@testing-library/react@15.0.7(@types/react@18.3.2)(react-dom@18.2.0)(react@18.2.0): @@ -8590,7 +8905,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@testing-library/react@15.0.7(react-dom@18.3.1)(react@18.3.1): + /@testing-library/react@15.0.7(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): resolution: { integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==, @@ -8606,6 +8921,7 @@ packages: dependencies: '@babel/runtime': 7.24.5 '@testing-library/dom': 10.1.0 + '@types/react': 18.3.2 '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8621,7 +8937,7 @@ packages: '@testing-library/dom': '>=7.21.4' dependencies: '@testing-library/dom': 10.1.0 - dev: false + dev: true /@testing-library/user-event@14.5.2(@testing-library/dom@9.3.4): resolution: @@ -8718,7 +9034,7 @@ packages: } dev: true - /@turbo/gen@1.13.3(@types/node@20.12.12)(typescript@5.2.2): + /@turbo/gen@1.13.3(@types/node@20.14.2)(typescript@5.2.2): resolution: { integrity: sha512-l+EM1gGzckFMaaVQyj3BVRa0QJ+tpp8HfiHOhGpBWW3Vc0Hfj92AY87Di/7HGABa+HVY7ueatMi7DJG+zkJBYg==, @@ -8733,7 +9049,7 @@ packages: minimatch: 9.0.4 node-plop: 0.26.3 proxy-agent: 6.4.0 - ts-node: 10.9.2(@types/node@20.12.12)(typescript@5.2.2) + ts-node: 10.9.2(@types/node@20.14.2)(typescript@5.2.2) update-check: 1.5.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -8777,6 +9093,7 @@ packages: { integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, } + dev: true /@types/babel__core@7.20.5: resolution: @@ -8848,6 +9165,13 @@ packages: '@types/node': 20.12.12 dev: true + /@types/cookie@0.6.0: + resolution: + { + integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, + } + dev: true + /@types/cross-spawn@6.0.6: resolution: { @@ -9036,6 +9360,13 @@ packages: rxjs: 6.6.7 dev: true + /@types/js-cookie@3.0.6: + resolution: + { + integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==, + } + dev: false + /@types/json-schema@7.0.15: resolution: { @@ -9077,6 +9408,15 @@ packages: } dev: true + /@types/mute-stream@0.0.4: + resolution: + { + integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==, + } + dependencies: + '@types/node': 20.14.2 + dev: true + /@types/node@18.19.33: resolution: { @@ -9094,6 +9434,14 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.14.2: + resolution: + { + integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==, + } + dependencies: + undici-types: 5.26.5 + /@types/normalize-package-data@2.4.4: resolution: { @@ -9188,6 +9536,13 @@ packages: '@types/send': 0.17.4 dev: true + /@types/statuses@2.0.5: + resolution: + { + integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==, + } + dev: true + /@types/through@0.0.33: resolution: { @@ -9218,6 +9573,13 @@ packages: } dev: true + /@types/wrap-ansi@3.0.0: + resolution: + { + integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==, + } + dev: true + /@typescript-eslint/eslint-plugin@6.6.0(@typescript-eslint/parser@6.6.0)(eslint@8.57.0)(typescript@5.2.2): resolution: { @@ -9659,6 +10021,36 @@ packages: '@vanilla-extract/private': 1.0.4 dev: false + /@vanilla-extract/integration@7.1.4: + resolution: + { + integrity: sha512-/9RYhOVvr28Vn5pDahgfccFqlfepyogdlGg3cabR9kVvKHQdNkAFuPp2mx8EzPPI2D9ZIcPwfb3jp8t2Beo/Vw==, + } + dependencies: + '@babel/core': 7.24.5 + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) + '@vanilla-extract/babel-plugin-debug-ids': 1.0.5 + '@vanilla-extract/css': 1.15.1 + dedent: 1.5.3 + esbuild: 0.19.12 + eval: 0.1.8 + find-up: 5.0.0 + javascript-stringify: 2.1.0 + mlly: 1.7.0 + vite: 5.2.11(@types/node@20.14.2) + vite-node: 1.6.0 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /@vanilla-extract/integration@7.1.4(@types/node@20.12.12): resolution: { @@ -9689,6 +10081,36 @@ packages: - terser dev: true + /@vanilla-extract/integration@7.1.4(@types/node@20.14.2): + resolution: + { + integrity: sha512-/9RYhOVvr28Vn5pDahgfccFqlfepyogdlGg3cabR9kVvKHQdNkAFuPp2mx8EzPPI2D9ZIcPwfb3jp8t2Beo/Vw==, + } + dependencies: + '@babel/core': 7.24.5 + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.5) + '@vanilla-extract/babel-plugin-debug-ids': 1.0.5 + '@vanilla-extract/css': 1.15.1 + dedent: 1.5.3 + esbuild: 0.19.12 + eval: 0.1.8 + find-up: 5.0.0 + javascript-stringify: 2.1.0 + mlly: 1.7.0 + vite: 5.2.11(@types/node@20.14.2) + vite-node: 1.6.0(@types/node@20.14.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /@vanilla-extract/next-plugin@2.4.0(@types/node@20.12.12)(next@14.2.3)(webpack@5.91.0): resolution: { @@ -9698,7 +10120,7 @@ packages: next: '>=12.1.7' dependencies: '@vanilla-extract/webpack-plugin': 2.3.8(@types/node@20.12.12)(webpack@5.91.0) - next: 14.2.3(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1) + next: 14.2.3(@babel/core@7.23.7)(react-dom@18.3.1)(react@18.3.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -9760,6 +10182,28 @@ packages: - terser dev: true + /@vanilla-extract/vite-plugin@4.0.9(@types/node@20.14.2)(vite@5.2.11): + resolution: + { + integrity: sha512-O3SU6whsm01lD9Kwpkz9yF14u0SCF0jbGtvMpslXGDZ6f3B9oq0M6PViu94gEYy8Xt2B4y23NF8RCrMOwDn81g==, + } + peerDependencies: + vite: ^4.0.3 || ^5.0.0 + dependencies: + '@vanilla-extract/integration': 7.1.4(@types/node@20.14.2) + vite: 5.2.11(@types/node@20.14.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /@vanilla-extract/vite-plugin@4.0.9(vite@4.5.3): resolution: { @@ -9768,7 +10212,7 @@ packages: peerDependencies: vite: ^4.0.3 || ^5.0.0 dependencies: - '@vanilla-extract/integration': 7.1.4(@types/node@20.12.12) + '@vanilla-extract/integration': 7.1.4 vite: 4.5.3 transitivePeerDependencies: - '@types/node' @@ -9807,6 +10251,31 @@ packages: - terser dev: true + /@vanilla-extract/webpack-plugin@2.3.8(webpack@5.91.0): + resolution: + { + integrity: sha512-etdNKd+lB4QowW7tNLWFCkAnUUYLiJWRdcVfgwUyaYSl4IOD4SabBbY/0uTDVE3LQaCHAsiWGFflWwoKD9F1SQ==, + } + peerDependencies: + webpack: ^4.30.0 || ^5.20.2 + dependencies: + '@vanilla-extract/integration': 7.1.4 + debug: 4.3.4 + loader-utils: 2.0.4 + picocolors: 1.0.1 + webpack: 5.91.0(esbuild@0.20.2) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /@vitejs/plugin-react-swc@3.7.0(vite@5.2.11): resolution: { @@ -9816,7 +10285,7 @@ packages: vite: ^4 || ^5 dependencies: '@swc/core': 1.5.7 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.11(@types/node@20.14.2) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -9840,18 +10309,18 @@ packages: - supports-color dev: true - /@vitejs/plugin-react@4.2.1(vite@5.2.11): + /@vitejs/plugin-react@4.3.1(vite@5.2.11): resolution: { - integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==, + integrity: sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==, } engines: { node: ^14.18.0 || >=16.0.0 } peerDependencies: vite: ^4.2.0 || ^5.0.0 dependencies: - '@babel/core': 7.23.7 - '@babel/plugin-transform-react-jsx-self': 7.24.5(@babel/core@7.23.7) - '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.23.7) + '@babel/core': 7.24.5 + '@babel/plugin-transform-react-jsx-self': 7.24.5(@babel/core@7.24.5) + '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.5) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 vite: 5.2.11(@types/node@20.12.12) @@ -9936,7 +10405,7 @@ packages: pathe: 1.1.2 picocolors: 1.0.1 sirv: 2.0.4 - vitest: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) + vitest: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0) dev: true /@vitest/utils@1.3.1: @@ -10555,6 +11024,7 @@ packages: integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, } engines: { node: '>=10' } + dev: true /ansi-styles@6.2.1: resolution: @@ -11604,7 +12074,6 @@ packages: integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==, } engines: { node: '>= 12' } - dev: false /client-only@0.0.1: resolution: @@ -11612,6 +12081,18 @@ packages: integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, } + /cliui@8.0.1: + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: '>=12' } + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + /clone-deep@4.0.1: resolution: { @@ -11914,6 +12395,14 @@ packages: } dev: true + /cookie@0.5.0: + resolution: + { + integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==, + } + engines: { node: '>= 0.6' } + dev: true + /cookie@0.6.0: resolution: { @@ -12639,6 +13128,7 @@ packages: { integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, } + dev: true /dom-accessibility-api@0.6.3: resolution: @@ -13805,7 +14295,7 @@ packages: eslint-rule-composer: 0.3.0 dev: true - /eslint-plugin-vitest@0.2.8(eslint@8.57.0)(typescript@5.2.2)(vite@5.2.11)(vitest@1.6.0): + /eslint-plugin-vitest@0.2.8(eslint@8.57.0)(typescript@5.2.2)(vitest@1.6.0): resolution: { integrity: sha512-q8s4tStyKtn3gXf+8nf1ZYTHhoCXKdnozZzp6u8b4ni5v68Y4vxhNh4Z8njUfNjEY8HoPBB77MazHMR23IPb+g==, @@ -13821,8 +14311,7 @@ packages: dependencies: '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.2.2) eslint: 8.57.0 - vite: 5.2.11(@types/node@20.12.12) - vitest: 1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0) + vitest: 1.6.0 transitivePeerDependencies: - supports-color - typescript @@ -14561,6 +15050,14 @@ packages: } engines: { node: '>=6.9.0' } + /get-caller-file@2.0.5: + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + dev: true + /get-east-asian-width@1.2.0: resolution: { @@ -14870,7 +15367,6 @@ packages: { integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==, } - dev: true /gopd@1.0.1: resolution: @@ -14968,6 +15464,14 @@ packages: engines: { node: '>= 10.x' } dev: false + /graphql@16.8.1: + resolution: + { + integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==, + } + engines: { node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0 } + dev: true + /gunzip-maybe@1.4.2: resolution: { @@ -15114,6 +15618,13 @@ packages: upper-case: 1.1.3 dev: true + /headers-polyfill@4.0.3: + resolution: + { + integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, + } + dev: true + /hoist-non-react-statics@3.3.2: resolution: { @@ -15796,6 +16307,13 @@ packages: engines: { node: '>= 0.4' } dev: true + /is-node-process@1.2.0: + resolution: + { + integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==, + } + dev: true + /is-number-object@1.0.7: resolution: { @@ -16100,7 +16618,7 @@ packages: } engines: { node: '>= 10.13.0' } dependencies: - '@types/node': 20.12.12 + '@types/node': 20.14.2 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -16120,6 +16638,14 @@ packages: engines: { node: '>=10' } dev: false + /js-cookie@3.0.5: + resolution: + { + integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==, + } + engines: { node: '>=14' } + dev: false + /js-tokens@4.0.0: resolution: { @@ -17026,6 +17552,7 @@ packages: integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, } hasBin: true + dev: true /magic-string@0.27.0: resolution: @@ -17468,6 +17995,52 @@ packages: msgpackr-extract: 3.0.2 dev: false + /msw-storybook-addon@2.0.2(msw@2.3.1): + resolution: + { + integrity: sha512-sdw++X+AoUbaG2ku493ViVqCA/LfqnybXsKXyPUrF3ZS/x8BqGBnkBLmT/0SHCC5zIO3Vfm5zlclAxnhqOOikQ==, + } + peerDependencies: + msw: ^2.0.0 + dependencies: + is-node-process: 1.2.0 + msw: 2.3.1(typescript@5.2.2) + dev: true + + /msw@2.3.1(typescript@5.2.2): + resolution: + { + integrity: sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==, + } + engines: { node: '>=18' } + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.7.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.1.9 + '@mswjs/cookies': 1.1.0 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.8.1 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.2.2 + strict-event-emitter: 0.5.1 + type-fest: 4.20.0 + typescript: 5.2.2 + yargs: 17.7.2 + dev: true + /muggle-string@0.3.1: resolution: { @@ -17488,7 +18061,6 @@ packages: integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==, } engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } - dev: false /mz@2.7.0: resolution: @@ -17560,7 +18132,7 @@ packages: engines: { node: '>= 0.4.0' } dev: true - /next@14.2.3(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1): + /next@14.2.3(@babel/core@7.23.7)(react-dom@18.3.1)(react@18.3.1): resolution: { integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==, @@ -17589,7 +18161,7 @@ packages: 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.5)(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.23.7)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.3 '@next/swc-darwin-x64': 14.2.3 @@ -18095,6 +18667,13 @@ packages: } engines: { node: '>=0.10.0' } + /outvariant@1.4.2: + resolution: + { + integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==, + } + dev: true + /p-cancelable@3.0.0: resolution: { @@ -18384,6 +18963,13 @@ packages: } dev: true + /path-to-regexp@6.2.2: + resolution: + { + integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==, + } + dev: true + /path-type@4.0.0: resolution: { @@ -19262,6 +19848,7 @@ packages: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 + dev: true /pretty-format@29.7.0: resolution: @@ -19647,6 +20234,18 @@ packages: } dev: false + /react-hook-form@7.51.5(react@18.3.1): + resolution: + { + integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==, + } + engines: { node: '>=12.22.0' } + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.3.1 + dev: false + /react-is@16.13.1: resolution: { @@ -19658,6 +20257,7 @@ packages: { integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, } + dev: true /react-is@18.1.0: resolution: @@ -20029,6 +20629,14 @@ packages: unist-util-visit: 5.0.0 dev: true + /require-directory@2.1.1: + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: '>=0.10.0' } + dev: true + /require-from-string@2.0.2: resolution: { @@ -20852,13 +21460,6 @@ packages: } dev: true - /std@0.1.40: - resolution: - { - integrity: sha512-wUf57hkDGCoVShrhPA8Q7lAg2Qosk+FaMlECmAsr1A4/rL2NRXFHQGBcgMUFKVkPEemJFW9gzjCQisRty14ohg==, - } - dev: false - /stop-iteration-iterator@1.0.0: resolution: { @@ -20920,6 +21521,13 @@ packages: bare-events: 2.2.2 dev: false + /strict-event-emitter@0.5.1: + resolution: + { + integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==, + } + dev: true + /string-argv@0.3.2: resolution: { @@ -21133,7 +21741,7 @@ packages: webpack: 5.91.0(esbuild@0.20.2) dev: true - /styled-jsx@5.1.1(@babel/core@7.24.5)(react@18.3.1): + /styled-jsx@5.1.1(@babel/core@7.23.7)(react@18.3.1): resolution: { integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==, @@ -21149,7 +21757,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.24.5 + '@babel/core': 7.23.7 client-only: 0.0.1 react: 18.3.1 @@ -21686,7 +22294,7 @@ packages: } dev: false - /ts-node@10.9.2(@types/node@20.12.12)(typescript@5.2.2): + /ts-node@10.9.2(@types/node@20.14.2)(typescript@5.2.2): resolution: { integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==, @@ -21708,7 +22316,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.12 + '@types/node': 20.14.2 acorn: 8.11.3 acorn-walk: 8.3.2 arg: 4.1.3 @@ -21720,10 +22328,10 @@ packages: yn: 3.1.1 dev: true - /tsconfck@3.0.3(typescript@5.2.2): + /tsconfck@3.1.0(typescript@5.2.2): resolution: { - integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==, + integrity: sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==, } engines: { node: ^18 || >=20 } hasBin: true @@ -21734,7 +22342,6 @@ packages: optional: true dependencies: typescript: 5.2.2 - dev: true /tsconfig-paths@3.15.0: resolution: @@ -21993,12 +22600,20 @@ packages: } engines: { node: '>=12.20' } - /type-is@1.6.18: + /type-fest@4.20.0: resolution: { - integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, + integrity: sha512-MBh+PHUHHisjXf4tlx0CFWoMdjx8zCMLJHOjnV1prABYZFHqtFOyauCIK2/7w4oIfwkF8iNhLtnJEfVY2vn3iw==, } - engines: { node: '>= 0.6' } + engines: { node: '>=16' } + dev: true + + /type-is@1.6.18: + resolution: + { + integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, + } + engines: { node: '>= 0.6' } dependencies: media-typer: 0.3.0 mime-types: 2.1.35 @@ -22095,15 +22710,6 @@ packages: dev: true optional: true - /ui@0.2.4: - resolution: - { - integrity: sha512-se2D+frb/i6JwFc7j72IEa0JADF8lfgc6ivOigaYkXHOuZRBtWe8R7UZ2YiXcbJ1dYNsjq40dn39rlNRxmVdPw==, - } - dependencies: - std: 0.1.40 - dev: false - /unbox-primitive@1.0.2: resolution: { @@ -22482,6 +23088,30 @@ packages: engines: { node: '>= 0.8' } dev: true + /vite-node@1.6.0: + resolution: + { + integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.2.11(@types/node@20.14.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@1.6.0(@types/node@20.12.12): resolution: { @@ -22506,7 +23136,31 @@ packages: - terser dev: true - /vite-plugin-dts@3.9.1(@types/node@20.12.12)(typescript@5.2.2)(vite@5.2.11): + /vite-node@1.6.0(@types/node@20.14.2): + resolution: + { + integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.1 + vite: 5.2.11(@types/node@20.14.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite-plugin-dts@3.9.1(@types/node@20.14.2)(typescript@5.2.2)(vite@5.2.11): resolution: { integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==, @@ -22519,14 +23173,14 @@ packages: vite: optional: true dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.12.12) + '@microsoft/api-extractor': 7.43.0(@types/node@20.14.2) '@rollup/pluginutils': 5.1.0 '@vue/language-core': 1.8.27(typescript@5.2.2) debug: 4.3.4 kolorist: 1.8.0 magic-string: 0.30.10 typescript: 5.2.2 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.11(@types/node@20.14.2) vue-tsc: 1.8.27(typescript@5.2.2) transitivePeerDependencies: - '@types/node' @@ -22547,9 +23201,29 @@ packages: fast-glob: 3.3.2 fs-extra: 11.2.0 picocolors: 1.0.1 - vite: 5.2.11(@types/node@20.12.12) + vite: 5.2.11(@types/node@20.14.2) dev: false + /vite-tsconfig-paths@4.3.2(typescript@5.2.2)(vite@4.5.3): + resolution: + { + integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==, + } + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + debug: 4.3.4 + globrex: 0.1.2 + tsconfck: 3.1.0(typescript@5.2.2) + vite: 4.5.3 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /vite-tsconfig-paths@4.3.2(typescript@5.2.2)(vite@5.2.11): resolution: { @@ -22563,12 +23237,12 @@ packages: dependencies: debug: 4.3.4 globrex: 0.1.2 - tsconfck: 3.0.3(typescript@5.2.2) + tsconfck: 3.1.0(typescript@5.2.2) vite: 5.2.11(@types/node@20.12.12) transitivePeerDependencies: - supports-color - typescript - dev: true + dev: false /vite@4.5.3: resolution: @@ -22646,7 +23320,103 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vitest@1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0)(jsdom@24.0.0): + /vite@5.2.11(@types/node@20.14.2): + resolution: + { + integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.14.2 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.17.2 + optionalDependencies: + fsevents: 2.3.3 + + /vitest@1.6.0: + resolution: + { + integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.14.2) + vite-node: 1.6.0 + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vitest@1.6.0(@types/node@20.12.12)(@vitest/ui@1.6.0): resolution: { integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==, @@ -22685,6 +23455,65 @@ packages: chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.12.12) + vite-node: 1.6.0(@types/node@20.12.12) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vitest@1.6.0(@types/node@20.12.12)(jsdom@24.0.0): + resolution: + { + integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.12.12 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 jsdom: 24.0.0 local-pkg: 0.5.0 magic-string: 0.30.10 @@ -22707,6 +23536,66 @@ packages: - terser dev: true + /vitest@1.6.0(@types/node@20.14.2)(@vitest/ui@1.6.0): + resolution: + { + integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.14.2 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/ui': 1.6.0(vitest@1.6.0) + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.8.0 + tinypool: 0.8.4 + vite: 5.2.11(@types/node@20.14.2) + vite-node: 1.6.0(@types/node@20.14.2) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vue-template-compiler@2.7.16: resolution: { @@ -23117,6 +24006,14 @@ packages: } dev: false + /y18n@5.0.8: + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: '>=10' } + dev: true + /yallist@3.1.1: resolution: { @@ -23144,6 +24041,30 @@ packages: } engines: { node: '>= 14' } + /yargs-parser@21.1.1: + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: '>=12' } + dev: true + + /yargs@17.7.2: + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: '>=12' } + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + /yn@3.1.1: resolution: { @@ -23183,6 +24104,12 @@ packages: commander: 9.5.0 dev: true + /zod@3.23.8: + resolution: + { + integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==, + } + /zustand@4.5.2(@types/react@18.3.2)(react@18.2.0): resolution: { diff --git a/turbo.json b/turbo.json index 781f274..0eea426 100644 --- a/turbo.json +++ b/turbo.json @@ -9,6 +9,9 @@ "lint": { "dependsOn": ["^lint"] }, + "test": { + "dependsOn": ["^test"] + }, "dev": { "cache": false, "persistent": true