diff --git a/apps/web/src/app/(afterLogin)/layout.tsx b/apps/web/src/app/(afterLogin)/layout.tsx
index ed7de26..7a50ac1 100644
--- a/apps/web/src/app/(afterLogin)/layout.tsx
+++ b/apps/web/src/app/(afterLogin)/layout.tsx
@@ -1,6 +1,6 @@
import { PropsWithChildren } from 'react'
-import { userService, UserStatus } from '@vook-client/api'
-import { cookies, headers } from 'next/headers'
+import { UserInfoResponse, userService, UserStatus } from '@vook-client/api'
+import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
@@ -15,24 +15,30 @@ import { mainArea } from './layout.css'
const Layout = async ({ children }: PropsWithChildren) => {
const cookieStore = cookies()
- const isAuthorization = headers().get('X-AuthConfirm')
-
- if (isAuthorization !== 'confirmed') {
- redirect('/login')
- }
const access = cookieStore.get('access')?.value || ''
const refresh = cookieStore.get('refresh')?.value || ''
- if (!access && !refresh) {
+ if (!access || !refresh) {
redirect('/login')
}
const queryClient = getQueryClient()
+
queryClient.setQueryData(['access'], access)
queryClient.setQueryData(['refresh'], refresh)
- const user = await userService.userInfo(queryClient)
+ let user: UserInfoResponse
+
+ try {
+ user = await userService.userInfo(queryClient)
+ } catch {
+ redirect('/login')
+ }
+
+ if (user.result.onboardingCompleted === false) {
+ redirect('/onboarding')
+ }
if (user.result.status !== UserStatus.Registered) {
redirect('/signup')
diff --git a/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts b/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts
index 4a3b445..012897d 100644
--- a/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts
+++ b/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts
@@ -36,7 +36,7 @@ export const termTitleContainer = style({
})
export const highlightHit = style({
- backgroundColor: vars.colors['component-alternative'],
+ backgroundColor: vars.colors['semantic-line-alternative'],
})
export const highlight = style({
diff --git a/apps/web/src/app/(afterLogin)/workspace/layout.tsx b/apps/web/src/app/(afterLogin)/workspace/layout.tsx
index fb5e247..2929f55 100644
--- a/apps/web/src/app/(afterLogin)/workspace/layout.tsx
+++ b/apps/web/src/app/(afterLogin)/workspace/layout.tsx
@@ -1,36 +1,20 @@
import { PropsWithChildren } from 'react'
-import { userService, UserStatus } from '@vook-client/api'
-import { cookies, headers } from 'next/headers'
-import { redirect } from 'next/navigation'
+import { cookies } from 'next/headers'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { getQueryClient } from '@/utils/react-query'
-const Layout = async ({ children }: PropsWithChildren) => {
+const Layout = ({ children }: PropsWithChildren) => {
const cookieStore = cookies()
- const isAuthorization = headers().get('X-AuthConfirm')
-
- if (isAuthorization !== 'confirmed') {
- redirect('/login')
- }
const access = cookieStore.get('access')?.value || ''
const refresh = cookieStore.get('refresh')?.value || ''
- if (!access && !refresh) {
- redirect('/login')
- }
-
const queryClient = getQueryClient()
+
queryClient.setQueryData(['access'], access)
queryClient.setQueryData(['refresh'], refresh)
- const user = await userService.userInfo(queryClient)
-
- if (user.result.status !== UserStatus.Registered) {
- redirect('/signup')
- }
-
const dehydrateState = dehydrate(queryClient)
return (
diff --git a/apps/web/src/app/(beforeLogin)/auth/page.tsx b/apps/web/src/app/(beforeLogin)/auth/page.tsx
new file mode 100644
index 0000000..8459836
--- /dev/null
+++ b/apps/web/src/app/(beforeLogin)/auth/page.tsx
@@ -0,0 +1,7 @@
+import { redirect } from 'next/navigation'
+
+const Page = () => {
+ return redirect('/login')
+}
+
+export default Page
diff --git a/apps/web/src/app/(beforeLogin)/auth/token/page.tsx b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx
index 1cdd8f0..5bb31b5 100644
--- a/apps/web/src/app/(beforeLogin)/auth/token/page.tsx
+++ b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx
@@ -32,14 +32,6 @@ const AuthCallbackPage = ({
secure: true,
expires: new Date('2038-01-19T03:14:07.000Z'),
})
- window.postMessage(
- {
- from: 'vook-web',
- access,
- refresh,
- },
- '*',
- )
queryClient.setQueryData(['access'], access)
queryClient.setQueryData(['refresh'], refresh)
diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.tsx b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx
index 0e2c4b7..b5f9566 100644
--- a/apps/web/src/app/(onboarding)/onboarding/job/page.tsx
+++ b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx
@@ -6,12 +6,10 @@ import {
SelectBox,
Text,
} from '@vook-client/design-system'
-import React from 'react'
+import React, { useEffect } 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'
@@ -76,10 +74,17 @@ const OnboardingJobPage = () => {
},
)
- const onSubmitFunnel = () => {
- if (!mutation.isPending || !mutation.isSuccess) {
- mutation.mutate()
+ useEffect(() => {
+ window.onpopstate = () => {
+ if (location.pathname === '/onboarding/job') {
+ alert('뒤로가기를 통한 접근을 감지하여 페이지를 이동합니다.')
+ router.push('/workspace')
+ }
}
+ }, [router])
+
+ const onSubmitFunnel = () => {
+ mutation.mutate()
}
const onClickJob = (job: OnboardingJob) => {
@@ -122,22 +127,20 @@ const OnboardingJobPage = () => {
건너뛰기
-
-
-
+
)
diff --git a/apps/web/src/app/(onboarding)/onboarding/layout.tsx b/apps/web/src/app/(onboarding)/onboarding/layout.tsx
index d5c3e33..c83faa8 100644
--- a/apps/web/src/app/(onboarding)/onboarding/layout.tsx
+++ b/apps/web/src/app/(onboarding)/onboarding/layout.tsx
@@ -10,21 +10,29 @@ 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) {
+ if (!accessToken || !refreshToken) {
redirect('/login')
}
const queryClient = getQueryClient()
+
queryClient.setQueryData(['access'], accessToken)
queryClient.setQueryData(['refresh'], refreshToken)
- const userInfo = await userService.userInfo(queryClient)
+ let userInfo
+
+ try {
+ userInfo = await userService.userInfo(queryClient)
+ } catch {
+ redirect('/login')
+ }
if (userInfo.result.onboardingCompleted) {
- redirect('/')
+ redirect('/workspace')
}
return (
diff --git a/apps/web/src/components/InitialSetting/InitialSetting.tsx b/apps/web/src/components/InitialSetting/InitialSetting.tsx
index 5292b75..19fc9f8 100644
--- a/apps/web/src/components/InitialSetting/InitialSetting.tsx
+++ b/apps/web/src/components/InitialSetting/InitialSetting.tsx
@@ -2,14 +2,20 @@
import { baseFetcher } from '@vook-client/api'
import { useLayoutEffect } from 'react'
+import Cookies from 'js-cookie'
+import { useQueryClient } from '@tanstack/react-query'
import { useToast } from '@/hooks/useToast'
export const InitialSetting = () => {
const { addToast } = useToast()
+ const client = useQueryClient()
useLayoutEffect(() => {
// eslint-disable-next-line promise/prefer-await-to-callbacks
+ client.setQueryData(['access'], Cookies.get('access'))
+ client.setQueryData(['refresh'], Cookies.get('refresh'))
+
baseFetcher.setUnAuthorizedHandler(() => {
location.href = '/login'
})
@@ -19,6 +25,14 @@ export const InitialSetting = () => {
type: 'error',
})
})
+ baseFetcher.setTokenRefreshHandler((access, refresh) => {
+ Cookies.set('access', access, {
+ expires: new Date('2038-01-19T03:14:07.000Z'),
+ })
+ Cookies.set('refresh', refresh, {
+ expires: new Date('2038-01-19T03:14:07.000Z'),
+ })
+ })
}, [addToast])
return null
diff --git a/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx b/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx
index b559b6a..41f76d9 100644
--- a/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx
+++ b/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx
@@ -27,7 +27,7 @@ export const ProfileEditForm = () => {
const userEditMutation = useEditUserMutation(
{
- nickname,
+ nickname: nickname.trim(),
},
{
onSuccess: () => {
@@ -56,7 +56,7 @@ export const ProfileEditForm = () => {
useEffect(
function checkValidateNickname() {
- const isBlankNickname = nickname.length === 0
+ const isBlankNickname = nickname.trim().length === 0
const isSameNickname = nickname === user?.nickname
if (isBlankNickname || isSameNickname) {
diff --git a/apps/web/src/components/SignUpForm/SignUpForm.tsx b/apps/web/src/components/SignUpForm/SignUpForm.tsx
index 41f960c..5e57d1c 100644
--- a/apps/web/src/components/SignUpForm/SignUpForm.tsx
+++ b/apps/web/src/components/SignUpForm/SignUpForm.tsx
@@ -80,13 +80,11 @@ export const SignUpForm = () => {
[signUpMutation.isPending],
)
- const canSubmit = useMemo(
- () =>
- !formState.isValid ||
- signUpMutation.isPending ||
- signUpMutation.isSuccess,
- [formState.isValid, signUpMutation.isPending, signUpMutation.isSuccess],
- )
+ const canSubmit =
+ watch('nickname').trim().length === 0 ||
+ !formState.isValid ||
+ signUpMutation.isPending ||
+ signUpMutation.isSuccess
if (!userInfoQuery.data) {
return null
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
deleted file mode 100644
index 340966c..0000000
--- a/apps/web/src/middleware.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { NextRequest, NextResponse } from 'next/server'
-import {
- ACCESS_TOKEN_HEADER_KEY,
- REFRESH_TOKEN_HEADER_KEY,
- 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',
- [REFRESH_TOKEN_HEADER_KEY]: refresh,
- },
- },
- )
- if (res.ok) {
- newAccessToken = res.headers.get(ACCESS_TOKEN_HEADER_KEY)
- newRefreshToken = res.headers.get(REFRESH_TOKEN_HEADER_KEY)
-
- finalResponse.cookies.set('access', newAccessToken!, {
- expires: new Date('2038-01-19T03:14:07.000Z'),
- maxAge: 60 * 60 * 24 * 365 * 20,
- })
- finalResponse.cookies.set('refresh', newRefreshToken!, {
- expires: new Date('2038-01-19T03:14:07.000Z'),
- maxAge: 60 * 60 * 24 * 365 * 20,
- })
- } 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',
- [ACCESS_TOKEN_HEADER_KEY]: 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
- }
-
- finalResponse.headers.set('X-AuthConfirm', 'confirmed')
-
- return finalResponse
- }
-
-const onlyRegisteredMatch = [
- '/onboarding',
- '/user/edit',
- '/workspace',
- '/vocabulary/',
-]
-
-const onlyRegisteredMiddleware = checkUserStatusMiddleware([
- UserStatus.Registered,
- UserStatus.SocialLoginCompleted,
-])
-
-const onlyUnregisteredSocialUser = checkUserStatusMiddleware([
- UserStatus.SocialLoginCompleted,
- UserStatus.Withdrawn,
-])
-
-export async function middleware(req: NextRequest) {
- const requestHeaders = new Headers(req.headers)
- requestHeaders.set('X-pathname', req.nextUrl.pathname)
-
- const response = NextResponse.next({
- request: {
- headers: requestHeaders,
- },
- })
-
- if (
- onlyRegisteredMatch.some((url) => req.nextUrl.pathname.includes(url)) ||
- req.nextUrl.pathname.includes('/vocabulary/')
- ) {
- return onlyRegisteredMiddleware(req, response, '/login')
- }
-
- if (req.nextUrl.pathname === '/signup') {
- return onlyUnregisteredSocialUser(req, response, '/login')
- }
-
- return response
-}
diff --git a/packages/api/src/lib/fetcher.ts b/packages/api/src/lib/fetcher.ts
index a58e651..42c8e7c 100644
--- a/packages/api/src/lib/fetcher.ts
+++ b/packages/api/src/lib/fetcher.ts
@@ -115,8 +115,9 @@ export class Fetcher {
if (response.ok) {
data = (await response.json()) as Promise
} else {
- const error = (await response.json()) as { code: string }
- throw new Error(error.code)
+ const error = (await response.json()) as { result: string }
+
+ throw new Error(error.result)
}
} catch (error) {
// eslint-disable-next-line no-console
diff --git a/packages/api/src/services/user/queries.ts b/packages/api/src/services/user/queries.ts
index e63f4c1..4c8ad3b 100644
--- a/packages/api/src/services/user/queries.ts
+++ b/packages/api/src/services/user/queries.ts
@@ -20,6 +20,7 @@ import {
export const userOptions = {
userInfo: (client: QueryClient) => ({
queryKey: [],
+ staleTime: 0,
queryFn: () => userService.userInfo(client),
}),
onboarding: (client: QueryClient, dto: OnboardingDTO) => ({
diff --git a/packages/design-system/src/tokens/colors.ts b/packages/design-system/src/tokens/colors.ts
index f6d2136..6e992b7 100644
--- a/packages/design-system/src/tokens/colors.ts
+++ b/packages/design-system/src/tokens/colors.ts
@@ -12,7 +12,7 @@ const semantic = {
'semantic-label-disabled': 'rgba(22, 23, 25, 0.16)',
/* line */
'semantic-line-normal': 'rgba(112, 115, 124, 0.22)',
- 'semantic-line-alternative': 'rgba(112, 115, 124, 0.08)',
+ 'semantic-line-alternative': 'rgba(112, 115, 124, 0.05)',
}
const link = {