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 (
+
+
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+
+ 용어집 1
+
+
+ 용어집 2
+
+
+
+
+ )
+}
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 0000000..58251dd
Binary files /dev/null and b/apps/workshop/public/logo.png differ
diff --git a/apps/workshop/public/mockServiceWorker.js b/apps/workshop/public/mockServiceWorker.js
new file mode 100644
index 0000000..24fe3a2
--- /dev/null
+++ b/apps/workshop/public/mockServiceWorker.js
@@ -0,0 +1,284 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ * - Please do NOT serve this file on production.
+ */
+
+const PACKAGE_VERSION = '2.3.1'
+const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+self.addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+self.addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+self.addEventListener('message', async function (event) {
+ const clientId = event.source.id
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: true,
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ 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(Button )
+ const { getByTitle } = render(Button )
// when & then
- expect(getAllByTitle('X')[0]).toBeInTheDocument()
+ expect(getByTitle('X')).toBeInTheDocument()
})
it('Button은 suffixIcon이 있을 때 정상적으로 렌더링된다.', () => {
// given
- const { getAllByTitle } = render(Button )
+ const { getByTitle } = render(Button )
// 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 (
-
- {prefixIcon && }
- {onlySuffixIcon && (
-
-
-
- )}
-
- {children}
-
- {suffixIcon && }
- {onlyPrefixIcon && (
-
-
-
- )}
-
- )
-}
+ return (
+
+ {prefixIcon && }
+
+ {children}
+
+ {suffixIcon && }
+
+ )
+ },
+)
+
+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 (
+
+
+ {label}
+
+ {inputElement}
+
+ )
+ }
+
+ 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 && }
+
+
+ {children}
+
+ {
+ setSelected((prev) => !prev)
+ }}
+ />
+ {suffixIcon && }
+
+
+ )
+ },
+)
+
+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