Skip to content

Commit

Permalink
[Feat] board auth (#177)
Browse files Browse the repository at this point in the history
close: #165 

* ✨ Feat: 로그인 시 내 정보 가져오기 추가

- 세션 스토리지에서 관리
- 로그아웃 시 제거

related to: #165

* ✨ Feat(activity): 게시판 생성시 권한 확인 추가

related to: #165

* 💄 Design(create-board): 게시판 이용자 input label UI 수정

* ✨ Feat(create-board): 권한 없을 때 에러 처리 추가

relatd to: #165
  • Loading branch information
ppochaco authored Aug 16, 2024
1 parent bc950b2 commit 7c447d1
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import { useRouter } from 'next/navigation'

import { useAuthStore } from '@/store/auth'
import { useMyInfoStore } from '@/store/myInfo'

export const LogoutButton = () => {
const router = useRouter()
const clearAccessToken = useAuthStore((state) => state.clearAccessToken)
const clearMyInfo = useMyInfoStore((state) => state.clearMyInfo)

const onClick = () => {
clearAccessToken()
clearMyInfo()
router.refresh()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { usePathname } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'

import { Button } from '@/components/ui/button'
import { DATA_ERROR_MESSAGES } from '@/constant/errorMessage'
Expand All @@ -13,20 +13,25 @@ const ActivityErrorFallback = ({
resetErrorBoundary,
}: FallbackProps) => {
const pathName = usePathname()
const router = useRouter()

if (error?.message === DATA_ERROR_MESSAGES.ACTIVITY_NOT_FOUND) {
const semesterName = getSemesterNameFromPath(pathName)
const currentSemester = useCurrentSemester(semesterName)
const { status } = useGetActivities(Number(currentSemester.semesterId))
const {} = useGetActivities(Number(currentSemester.semesterId))

if (status === 'pending') return <div>loading...</div>
resetErrorBoundary()
}

return (
<div className="flex flex-col gap-2">
<p>{error?.message} </p>
<Button onClick={() => resetErrorBoundary()}> 다시 시도 </Button>
<div className="flex flex-col items-center gap-6 pt-40">
<div>{error?.message} </div>
<div className="flex gap-2">
<Button variant="secondary" onClick={() => router.back()}>
뒤로가기
</Button>
<Button onClick={() => router.push('/auth/login')}> 로그인 </Button>
</div>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const ActivitySection = ({
const { data: activities, status } = useGetActivities(semesterId)
const currentActivity = useCurrentActivity(semesterId, activityId)

if (status === 'pending') return <div>loading...</div>
if (!activities?.length) return <div>활동이 없습니다.</div>

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'

import { Button } from '@/components/ui/button'
import { useMyInfoStore } from '@/store/myInfo'

export const CreateBoardButton = () => {
const pathName = usePathname()
const router = useRouter()
const { role } = useMyInfoStore((state) => state.getMyInfo())

return (
<div className="mb-20 flex w-full justify-end">
<Link href={`${pathName}/create-board`}>
<Button className="max-w-fit">게시판 생성하기</Button>
</Link>
<Button
className="max-w-fit"
onClick={() => router.push(`${pathName}/create-board`)}
disabled={!(role === '해구르르' || role === '팀장')}
>
게시판 생성하기
</Button>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,21 @@ type SelectMemberFieldProps = {
}

export const SelectMemberField = ({ name, label }: SelectMemberFieldProps) => {
const { users, status } = useGetUsers()
const { users } = useGetUsers()

const form = useFormContext()
const [selectedMember, setSelectedMember] = useState<User[]>([])

if (status === 'pending') return <div>loading...</div>
if (!users) return <div>?</div>
if (!users) return <div>유저가 없습니다.</div>

return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="flex flex-col">
<div className="flex flex-col md:flex-row md:items-center">
<Label className="text-md w-40">{label}</Label>
<div className="flex h-full flex-col md:flex-row md:items-start">
<Label className="text-md w-40 pt-2">{label}</Label>
<FormControl className="h-8 cursor-pointer">
<MultipleMemberSelect
options={users}
Expand Down
15 changes: 13 additions & 2 deletions src/app/auth/login/_components/LoginForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { getMyInfo } from '@/service/server'
import { loginAction } from '@/service/server/login'
import { useAuthStore } from '@/store/auth'
import { useMyInfoStore } from '@/store/myInfo'

import { LoginErrorMessage } from '~auth/login/_components/LoginErrorMesage'

Expand All @@ -23,6 +25,7 @@ export const LoginForm = () => {
const router = useRouter()
const { execute, result, isExecuting } = useAction(loginAction)
const setAccessToken = useAuthStore((state) => state.setAccessToken)
const setMyInfo = useMyInfoStore((state) => state.setMyInfo)

const form = useForm<LoginSchema>({
defaultValues: {
Expand All @@ -38,9 +41,17 @@ export const LoginForm = () => {
}

useEffect(() => {
if (result.data?.status === 200) {
if (result.data?.isSuccess) {
setAccessToken(result.data.token)
router.push('/')

const fetchAndStoreMyInfo = async () => {
const myInfo = await getMyInfo()
setMyInfo({ userName: myInfo.userName, role: myInfo.role })
}

fetchAndStoreMyInfo().then(() => {
router.push('/')
})
}
})

Expand Down
4 changes: 4 additions & 0 deletions src/constant/errorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export const DATA_ERROR_MESSAGES = {
ACTIVITY_NOT_FOUND: '활동을 불러오는 데 오류가 발생했습니다.',
BOARD_DETAIL_NOT_FOUND: '게시판 정보를 불러오는 데 오류가 발생했습니다.',
}

export const ACCESS_ERROR_MESSAGE = {
UNAUTHORIZED_ERROR: '해당 페이지에 접근 할 권한이 없습니다.',
}
4 changes: 2 additions & 2 deletions src/service/data/activity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { queryOptions, useQuery } from '@tanstack/react-query'
import { queryOptions, useQuery, useSuspenseQuery } from '@tanstack/react-query'

import { DATA_ERROR_MESSAGES } from '@/constant/errorMessage'
import { queryClient } from '@/service/components/ReactQueryClientProvider'
Expand All @@ -12,7 +12,7 @@ const activitiesQuery = (semesterId: number) =>
})

export const useGetActivities = (semesterId: number) => {
return useQuery(activitiesQuery(semesterId))
return useSuspenseQuery(activitiesQuery(semesterId))
}

export const useCurrentActivity = (semesterId: number, activityId: number) => {
Expand Down
4 changes: 2 additions & 2 deletions src/service/data/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { useSuspenseQuery } from '@tanstack/react-query'

import { getUsers } from '@/service/server/user'

Expand All @@ -7,7 +7,7 @@ export const useGetUsers = () => {
data: users,
status,
error,
} = useQuery({
} = useSuspenseQuery({
queryKey: ['users', 'active'],
queryFn: async () => getUsers(),
})
Expand Down
16 changes: 16 additions & 0 deletions src/service/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AUTHORIZATION_API } from '@/service/config'
import { Role } from '@/types/user'

type MyInfoResponse = {
userId: string
studentNumber: number
userName: string
role: Role
regDate: string
}

export const getMyInfo = async () => {
const response = await AUTHORIZATION_API.get<MyInfoResponse>('/users/me')

return response.data
}
2 changes: 1 addition & 1 deletion src/service/server/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const loginAction = actionClient

const accessToken = res.headers['authorization']

return { status: res.status, token: accessToken }
return { isSuccess: true, token: accessToken }
} catch (error) {
if (error instanceof AxiosError) {
const res = error.response
Expand Down
13 changes: 11 additions & 2 deletions src/service/server/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { AxiosError } from 'axios'

import { ACCESS_ERROR_MESSAGE } from '@/constant/errorMessage'
import { AUTHORIZATION_API } from '@/service/config'
import { User } from '@/types/user'

export const getUsers = async () => {
const response = await AUTHORIZATION_API.get<User[]>('/private/users')
try {
const response = await AUTHORIZATION_API.get<User[]>('/private/users')

return response.data
return response.data
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(ACCESS_ERROR_MESSAGE.UNAUTHORIZED_ERROR)
}
}
}
31 changes: 31 additions & 0 deletions src/store/myInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'

import { Role } from '@/types/user'

export type MyInfo = {
userName: string
role?: Role
}

interface MyInfoProps {
myInfo: MyInfo
setMyInfo: (myInfo: MyInfo) => void
clearMyInfo: () => void
getMyInfo: () => MyInfo
}

export const useMyInfoStore = create(
persist<MyInfoProps>(
(set, get) => ({
myInfo: { userName: '', role: undefined },
setMyInfo: (myInfo) => set({ myInfo }),
clearMyInfo: () => set({ myInfo: { userName: '', role: undefined } }),
getMyInfo: () => get().myInfo,
}),
{
name: 'my-info',
storage: createJSONStorage(() => sessionStorage),
},
),
)
4 changes: 3 additions & 1 deletion src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type User = {
userName: string
}

export type Role = '해구르르' | '팀장' | '일반'

export type ActiveUser = User & {
role: '해구르르' | '팀장' | '일반'
role: Role
}

0 comments on commit 7c447d1

Please sign in to comment.