- {/* 컨테이너 */}
-
-
-
초대 내역
-
-
-
- {totalPages} 페이지 중 {currentPage}
-
-
-
-
-
-
+
+
+
+
diff --git a/src/app/features/dashboard/components/edit/EditMember.tsx b/src/app/features/dashboard/components/edit/EditMember.tsx
index b5ed11d..ba12246 100644
--- a/src/app/features/dashboard/components/edit/EditMember.tsx
+++ b/src/app/features/dashboard/components/edit/EditMember.tsx
@@ -1,99 +1,134 @@
+'use client'
+
+import authHttpClient from '@lib/axios'
+import { cn } from '@lib/cn'
+import { getTeamId } from '@lib/getTeamId'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Image from 'next/image'
-import React from 'react'
-import { useState } from 'react'
+import { useParams } from 'next/navigation'
+import React, { useState } from 'react'
import { UserInfo } from '@/app/shared/components/common/UserInfo'
+import { fetchMembers, Member } from '@/app/shared/hooks/useMembers'
+import { showError, showSuccess } from '@/app/shared/lib/toast'
+
+import { PaginationHeader } from './PaginationHeader'
-import { mockMembers } from './mockMember'
+const PAGE_SIZE = 4
+const teamId = getTeamId()
-const PAGE_SIZE = 4 // 페이지당 표시할 구성원 수
+async function deleteMember(memberId: number): Promise
{
+ await authHttpClient.delete(`/${teamId}/members/${memberId}`)
+}
export default function EditMember() {
+ const queryClient = useQueryClient()
+ const { id: dashboardId } = useParams()
+ const dashboardIdStr = String(dashboardId)
+
const [currentPage, setCurrentPage] = useState(1)
- const totalPages = Math.ceil(mockMembers.length / PAGE_SIZE)
- const startIndex = (currentPage - 1) * PAGE_SIZE
- const paginationMembers = mockMembers.slice(
- startIndex,
- startIndex + PAGE_SIZE,
- )
+ const {
+ data: members = [],
+ isLoading,
+ isError,
+ } = useQuery({
+ queryKey: ['members', dashboardIdStr],
+ queryFn: () => fetchMembers(dashboardIdStr),
+ enabled: !!dashboardIdStr,
+ })
+
+ // 본인이 구성원으로 들어가기 때문에 0 페이지일 경우 X
+ const totalPages = Math.ceil(members.length / PAGE_SIZE)
+ const startIdx = (currentPage - 1) * PAGE_SIZE
+ const paginationMembers = members.slice(startIdx, startIdx + PAGE_SIZE)
- const handlePrev = () => {
- if (currentPage > 1) {
- setCurrentPage((prev) => prev - 1)
- }
+ function handlePrev() {
+ setCurrentPage((prev) => Math.max(prev - 1, 1))
}
- const handleNext = () => {
- if (currentPage < totalPages) {
- setCurrentPage((prev) => prev + 1)
- }
+ function handleNext() {
+ setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
+ const { mutate: removeMember, isPending: isDeleting } = useMutation({
+ mutationFn: (memberId: number) => deleteMember(memberId),
+ onSuccess: () => {
+ showSuccess('삭제에 성공하였습니다.')
+ queryClient.invalidateQueries({ queryKey: ['members', dashboardIdStr] })
+ },
+ onError: () => {
+ showError('삭제에 실패했습니다.')
+ },
+ })
+
return (
- {/* 컨테이너 */}
-
-
구성원
-
-
-
- {totalPages} 페이지 중 {currentPage}
-
-
-
-
-
+
diff --git a/src/app/features/dashboard/components/edit/PaginationHeader.tsx b/src/app/features/dashboard/components/edit/PaginationHeader.tsx
new file mode 100644
index 0000000..4a555b7
--- /dev/null
+++ b/src/app/features/dashboard/components/edit/PaginationHeader.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import Image from 'next/image'
+import React from 'react'
+
+type PaginationHeaderProps = {
+ currentPage: number
+ totalPages: number
+ title: string
+ onPrev: () => void
+ onNext: () => void
+ children?: React.ReactNode
+}
+
+export function PaginationHeader({
+ currentPage,
+ totalPages,
+ title,
+ onPrev,
+ onNext,
+ children,
+}: PaginationHeaderProps) {
+ return (
+
+
{title}
+
+
+
+ {totalPages} 페이지 중 {currentPage}
+
+
+
+ {children &&
{children}
}
+
+
+ )
+}
diff --git a/src/app/features/dashboard/components/edit/mockMember.js b/src/app/features/dashboard/components/edit/mockMember.js
deleted file mode 100644
index 83c5a2b..0000000
--- a/src/app/features/dashboard/components/edit/mockMember.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// mockMember.js
-
-export const mockMembers = [
- {
- nickname: '민준',
- imageUrl: '/images/profile.gif',
- },
- {
- nickname: '서연',
- imageUrl: null,
- },
- {
- nickname: 'James',
- imageUrl: null,
- },
- {
- nickname: '나연 ',
- imageUrl: '/images/profile.gif',
- },
- {
- nickname: 'Emily',
- imageUrl: null,
- },
- {
- nickname: 'Jenny',
- imageUrl: null,
- },
-]
diff --git a/src/app/shared/components/common/Avatar.tsx b/src/app/shared/components/common/Avatar.tsx
index 056e2ae..8b9904b 100644
--- a/src/app/shared/components/common/Avatar.tsx
+++ b/src/app/shared/components/common/Avatar.tsx
@@ -1,10 +1,10 @@
'use client'
+import { getColor } from '@lib/getColor' // 경로는 실제 위치에 맞게 조정
import Image from 'next/image'
import { useRef } from 'react'
import { useAuthStore } from '@/app/features/auth/store/useAuthStore'
-import { getColor } from '@/app/shared/lib/getColor'
type AvatarProps = {
size?: number
@@ -46,7 +46,6 @@ export function Avatar({ size = 36, name, imageUrl }: AvatarProps) {
profileImageUrl.current = user.profileImageUrl
}
- const initial = getInitial(nickname.current)
const bgColor = getColor(nickname.current, customColors.length)
return profileImageUrl.current ? (
@@ -71,7 +70,7 @@ export function Avatar({ size = 36, name, imageUrl }: AvatarProps) {
backgroundColor: customColors[bgColor],
}}
>
- {initial}
+ {getInitial(name)}
)
}
diff --git a/src/app/shared/components/common/CollaboratorItem.tsx b/src/app/shared/components/common/CollaboratorItem.tsx
index cc65c49..096deee 100644
--- a/src/app/shared/components/common/CollaboratorItem.tsx
+++ b/src/app/shared/components/common/CollaboratorItem.tsx
@@ -6,7 +6,7 @@ import { Avatar } from './Avatar'
type CollaboratorItemProps = {
nickname: string
- imageUrl?: string
+ imageUrl?: string | null
size?: number
className?: string
onClick?: () => void
@@ -21,7 +21,7 @@ export default function CollaboratorItem({
}: CollaboratorItemProps) {
return (
)
}
diff --git a/src/app/shared/components/common/UserInfo.tsx b/src/app/shared/components/common/UserInfo.tsx
index 118bab8..30e8418 100644
--- a/src/app/shared/components/common/UserInfo.tsx
+++ b/src/app/shared/components/common/UserInfo.tsx
@@ -5,20 +5,24 @@ import { useAuthStore } from '@/app/features/auth/store/useAuthStore'
import { Avatar } from './Avatar'
type UserInfoProps = {
- nickname: string
+ nickname?: string
imageUrl?: string | null
size?: number
}
-export function UserInfo({ size = 36 }: UserInfoProps) {
+export function UserInfo({ nickname, imageUrl, size = 36 }: UserInfoProps) {
const user = useAuthStore((state) => state.user)
- if (!user) return null // 또는 로딩 중 표시나 기본 UI
+ const displayNickname = nickname ?? user?.nickname ?? ''
+ const displayImage = imageUrl ?? user?.profileImageUrl ?? null
+
+ if (!displayNickname) return null // 사용자 정보가 없는 경우 렌더링하지 않음
return (
-
-
{user.nickname}
+ {/* Avatar에 nickname, profileImageUrl 모두 넘겨줌 */}
+
+
{displayNickname}
)
}
diff --git a/src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx b/src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx
index 1543cf6..47d6bbd 100644
--- a/src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx
+++ b/src/app/shared/components/common/header/Collaborator/CollaboratorList.tsx
@@ -1,57 +1,56 @@
-'use client' // Tooltip, CollaboratorItem 컴포넌트는 클라이언트에서만 사용되므로 'use client' 선언
+'use client'
+
+import { fetchMembers, Member } from '@hooks/useMembers'
+import { useQuery } from '@tanstack/react-query'
+import { useParams } from 'next/navigation'
+import React from 'react'
import CollaboratorItem from '../../CollaboratorItem'
-import Tooltip from './Tooltip' // 툴팁 기능
-
-// 임시 mock 협업자 데이터
-export const mockCollaborators = [
- { nickname: '홍길동', imageUrl: '/images/collaborator.png' },
- { nickname: '김철수', imageUrl: '/images/collaborator.png' },
- { nickname: '이영희', imageUrl: '/images/collaborator.png' },
- { nickname: '뚜비', imageUrl: '/images/collaborator.png' },
- { nickname: '두룹', imageUrl: '/images/collaborator.png' },
- { nickname: 'ㅋ', imageUrl: '/images/collaborator.png' },
-]
-
-// 협업자 배열을 선택적으로 전달받음
-type CollaboratorListProps = {
- collaborators?: {
- nickname: string
- imageUrl?: string
- }[]
-}
+import Tooltip from './Tooltip'
+
+const MAX_COLLABS = 4
+
+export default function CollaboratorList() {
+ const { id: dashboardId } = useParams()
+ const dashboardIdStr = String(dashboardId)
-// 협업자 리스트 컴포넌트
-export default function CollaboratorList({
- collaborators = mockCollaborators, // 전달된 협업자가 없을 경우 mock 데이터 사용
-}: CollaboratorListProps) {
- const MAX_VISIBLE = 4 // 최대 표시 협업자 수
- const visibleCollaborators = collaborators.slice(0, MAX_VISIBLE) // 앞에서부터 4명만 추출
- const extraCount = collaborators.length - MAX_VISIBLE // 초과 인원 계산
+ const {
+ data: members = [],
+ isLoading,
+ isError,
+ } = useQuery
({
+ queryKey: ['members', dashboardIdStr],
+ queryFn: () => fetchMembers(dashboardIdStr),
+ enabled: !!dashboardIdStr,
+ })
+
+ if (isLoading && isError) return null
+
+ // 프로필 이미지 및 닉네임만 필요한 경우
+ const visibleCollaborators = members.slice(0, MAX_COLLABS)
+ const extraCount = members.length - MAX_COLLABS
return (
- {/* 협업자들 가로 배치 & 간격 설정 */}
{visibleCollaborators.map((collab) => (
- // 각 협업자에 툴팁을 감싸서 닉네임 표시
-
+
))}
- {/* 초과 인원이 있을 경우 +N 표시 */}
+
{extraCount > 0 && (
- +{extraCount} {/* 초과 인원 수 출력 */}
+ +{extraCount}
diff --git a/src/app/shared/components/common/header/NavItem.tsx b/src/app/shared/components/common/header/NavItem.tsx
index 0dac158..705a0c2 100644
--- a/src/app/shared/components/common/header/NavItem.tsx
+++ b/src/app/shared/components/common/header/NavItem.tsx
@@ -20,14 +20,13 @@ export default function NavItem({
onClick,
iconSrc,
label,
- active,
className,
}: NavItemProps) {
const content = (
+ // 정적인 클래스만 쓸 경우 cn을 쓰지 않아도 되지만 외부에서 className 받는 경우 사용 권장
diff --git a/src/app/shared/components/common/header/RightHeaderNav.tsx b/src/app/shared/components/common/header/RightHeaderNav.tsx
index bff972f..4465675 100644
--- a/src/app/shared/components/common/header/RightHeaderNav.tsx
+++ b/src/app/shared/components/common/header/RightHeaderNav.tsx
@@ -2,30 +2,26 @@
import { useModalStore } from '@store/useModalStore'
import { useSelectedDashboardStore } from '@store/useSelectedDashboardStore'
-import { usePathname } from 'next/navigation'
import NavItem from './NavItem'
export default function RightHeaderNav() {
- const pathname = usePathname()
- const { modalType, openModal } = useModalStore()
+ const { openModal } = useModalStore()
const { selectedDashboard } = useSelectedDashboardStore()
return (
-
}
width="w-6"
diff --git a/src/app/shared/components/common/modal/CreateDashboardModal.tsx b/src/app/shared/components/common/modal/CreateDashboardModal.tsx
index e80c28a..7e26ccc 100644
--- a/src/app/shared/components/common/modal/CreateDashboardModal.tsx
+++ b/src/app/shared/components/common/modal/CreateDashboardModal.tsx
@@ -1,82 +1,34 @@
'use client'
-import { DASHBOARD_COLORS } from '@constants/colors'
-import authHttpClient from '@lib/axios'
import { useModalStore } from '@store/useModalStore'
-import Image from 'next/image'
-import { useRouter } from 'next/navigation'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect } from 'react'
-import { CreateDashboardRequest } from '@/types/dashboard'
+import { useDashboardForm } from '@/app/shared/hooks/useDashboardForm'
+
+import DashboardForm from '../../dashboard/DashboardForm'
export default function CreateDashboardModal() {
- const router = useRouter()
const { modalType, closeModal } = useModalStore()
const isModalOpen = modalType === 'createDashboard'
- const [formData, setFormData] = useState({
- title: '',
- color: DASHBOARD_COLORS[0],
- })
-
- const [isSubmitting, setIsSubmitting] = useState(false)
+ const {
+ formData,
+ isSubmitting,
+ handleChange,
+ handleColorSelect,
+ handleSubmit,
+ resetForm,
+ } = useDashboardForm('create')
+ // 모달 닫힐 때 form 초기화
useEffect(() => {
if (!isModalOpen) {
- setFormData({ title: '', color: DASHBOARD_COLORS[0] })
- setIsSubmitting(false)
- }
- }, [isModalOpen])
-
- if (!isModalOpen) {
- return null
- }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
-
- if (!formData.title || !formData.color) {
- return
- }
- try {
- setIsSubmitting(true)
-
- if (!process.env.NEXT_PUBLIC_TEAM_ID) {
- throw new Error('NEXT_PUBLIC_TEAM_ID 환경변수가 설정되지 않았습니다.')
- }
-
- const response = await authHttpClient.post(
- `/${process.env.NEXT_PUBLIC_TEAM_ID}/dashboards`,
- formData,
- )
-
- const data = response.data
-
- // 성공 시 대시보드 상세 페이지로 이동
- router.push(`/dashboard/${data.id}`)
- closeModal()
- } catch (error) {
- console.error('대시보드 생성 오류:', error)
- } finally {
- setIsSubmitting(false)
+ resetForm()
}
- }
-
- // 입력값 변경 처리
- const handleChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target
- setFormData((prev) => ({
- ...prev,
- [name]: value,
- }))
- }
+ }, [isModalOpen, resetForm])
- // 색상 선택 처리
- const handleColorSelect = (color: string) => {
- setFormData((prev) => ({ ...prev, color }))
- }
+ if (!isModalOpen) return null
- // 모달 외부 클릭 시 닫기
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
closeModal()
@@ -84,83 +36,25 @@ export default function CreateDashboardModal() {
}
return (
- // 모달 백드롭
- {/* 모달 컨테이너 */}
새로운 대시보드
-
-
+
{
+ handleSubmit(e)
+ closeModal()
+ }}
+ isSubmitting={isSubmitting}
+ submitText="생성"
+ showCancelButton
+ onCancel={closeModal}
+ />
)
diff --git a/src/app/shared/components/common/modal/CreateInvitationModal.tsx b/src/app/shared/components/common/modal/CreateInvitationModal.tsx
index c4dbc0c..bfebe02 100644
--- a/src/app/shared/components/common/modal/CreateInvitationModal.tsx
+++ b/src/app/shared/components/common/modal/CreateInvitationModal.tsx
@@ -2,6 +2,7 @@
import { inviteUser } from '@dashboard/api/invitation'
import { useModalStore } from '@store/useModalStore'
+import { useQueryClient } from '@tanstack/react-query'
import { AxiosError } from 'axios'
import { useParams } from 'next/navigation'
import { useState } from 'react'
@@ -12,14 +13,16 @@ export default function CreateInvitationModal() {
const { modalType, closeModal } = useModalStore()
const [email, setEmail] = useState('')
const { id: dashboardId } = useParams()
+ const teamId = process.env.NEXT_PUBLIC_TEAM_ID ?? ''
+ const queryClient = useQueryClient()
- const handleBackdropClick = (e: React.MouseEvent) => {
+ function handleBackdropClick(e: React.MouseEvent) {
if (e.target === e.currentTarget) {
closeModal()
}
}
- const handleSubmit = async (e: React.FormEvent) => {
+ async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!email) return
@@ -27,17 +30,23 @@ export default function CreateInvitationModal() {
if (!dashboardId) {
throw new Error('대시보드 ID가 없습니다.')
}
+
await inviteUser({ email, dashboardId: Number(dashboardId) })
+
+ // ✅ 초대 후 캐시 무효화 (초대 내역 새로고침)
+ queryClient.invalidateQueries({
+ queryKey: ['invitations', teamId, String(dashboardId)],
+ })
+
showSuccess('초대가 완료되었습니다.')
closeModal()
} catch (err: unknown) {
- // 에러 타입 안정성을 위해 axios 에러 타입으로 캐스팅
const error = err as AxiosError<{ message: string }>
showError(error?.response?.data?.message || '초대에 실패하였습니다.')
}
}
- if (!modalType) return null
+ if (modalType !== 'invite') return null
return (
diff --git a/src/app/shared/components/dashboard/DashboardForm.tsx b/src/app/shared/components/dashboard/DashboardForm.tsx
new file mode 100644
index 0000000..1788d25
--- /dev/null
+++ b/src/app/shared/components/dashboard/DashboardForm.tsx
@@ -0,0 +1,99 @@
+'use client'
+
+import Image from 'next/image'
+import React from 'react'
+
+import { DASHBOARD_COLORS } from '@/app/shared/constants/colors'
+
+import Input from '../Input'
+
+type DashboardFormProps = {
+ formData: { title: string; color: string }
+ onChange: (e: React.ChangeEvent
) => void
+ onColorSelect: (color: string) => void
+ onSubmit: (e: React.FormEvent) => void
+ isSubmitting: boolean
+ submitText: string
+ submitButtonWidth?: string
+ showCancelButton?: boolean
+ onCancel?: () => void
+}
+
+export default function DashboardForm({
+ formData,
+ onChange,
+ onColorSelect,
+ onSubmit,
+ isSubmitting,
+ submitText,
+ submitButtonWidth = 'w-256',
+ showCancelButton = false,
+ onCancel,
+}: DashboardFormProps) {
+ return (
+
+ )
+}
diff --git a/src/app/shared/hooks/useDashboard.ts b/src/app/shared/hooks/useDashboard.ts
index 4281181..8cd6563 100644
--- a/src/app/shared/hooks/useDashboard.ts
+++ b/src/app/shared/hooks/useDashboard.ts
@@ -1,10 +1,10 @@
// hooks/useDashboard.ts
'use client'
+import api from '@lib/axios'
import { useQuery } from '@tanstack/react-query'
-import api from '@/app/shared/lib/axios'
-import { DashboardListResponse } from '@/app/shared/types/dashboard'
+import { DashboardListResponse } from '@/types/dashboard'
export const useDashboard = () => {
return useQuery({
diff --git a/src/app/shared/hooks/useDashboardForm.ts b/src/app/shared/hooks/useDashboardForm.ts
new file mode 100644
index 0000000..bb528d5
--- /dev/null
+++ b/src/app/shared/hooks/useDashboardForm.ts
@@ -0,0 +1,109 @@
+// hooks/useDashboardForm.ts
+import { DASHBOARD_COLORS } from '@constants/colors'
+import api from '@lib/axios'
+import { getTeamId } from '@lib/getTeamId'
+import { showError } from '@lib/toast'
+import { useSelectedDashboardStore } from '@store/useSelectedDashboardStore'
+import { useQueryClient } from '@tanstack/react-query'
+import axios from 'axios'
+import { useRouter } from 'next/navigation'
+import { useEffect, useState } from 'react'
+
+import { CreateDashboardRequest } from '@/types/dashboard'
+
+type Mode = 'create' | 'edit'
+
+const teamId = getTeamId()
+
+export function useDashboardForm(mode: Mode) {
+ const router = useRouter()
+ const queryClient = useQueryClient()
+ const { selectedDashboard, setSelectedDashboard } =
+ useSelectedDashboardStore()
+
+ const [formData, setFormData] = useState({
+ title: '',
+ color: DASHBOARD_COLORS[0],
+ })
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ // 수정 모드일 때 초기값 설정
+ useEffect(() => {
+ if (mode === 'edit' && selectedDashboard) {
+ setFormData({
+ title: selectedDashboard.title,
+ color: selectedDashboard.color,
+ })
+ }
+ }, [mode, selectedDashboard])
+
+ // 대시보드 생성 모달 폼 초기화
+ const resetForm = () => {
+ setFormData({
+ title: '',
+ color: DASHBOARD_COLORS[0],
+ })
+ setIsSubmitting(false)
+ }
+
+ // 입력값 변경 핸들러
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }))
+ }
+
+ // 색상 선택 핸들러
+ const handleColorSelect = (color: string) => {
+ setFormData((prev) => ({ ...prev, color }))
+ }
+
+ // 폼 제출 핸들러
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ if (!formData.title || !formData.color) return
+
+ try {
+ setIsSubmitting(true)
+
+ if (mode === 'create') {
+ const response = await api.post(`/${teamId}/dashboards`, formData)
+ router.push(`/dashboard/${response.data.id}`)
+ } else if (mode === 'edit' && selectedDashboard?.id) {
+ const response = await api.put(
+ `/${teamId}/dashboards/${selectedDashboard.id}`,
+ formData,
+ )
+ const data = response.data
+ setSelectedDashboard(data)
+
+ // 캐시 무효화 및 페이지 강제 갱신
+ await queryClient.invalidateQueries({ queryKey: ['dashboards'] })
+ router.refresh()
+
+ router.push(`/dashboard/${data.id}/edit`)
+ }
+ } catch (error) {
+ const message = axios.isAxiosError(error)
+ ? error.response?.data?.message ||
+ '대시보드 요청 중 오류가 발생했습니다.'
+ : '알 수 없는 오류가 발생했습니다.'
+ showError(message)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return {
+ formData,
+ isSubmitting,
+ handleChange,
+ handleColorSelect,
+ handleSubmit,
+ resetForm,
+ selectedDashboard,
+ }
+}
diff --git a/src/app/shared/hooks/useMembers.ts b/src/app/shared/hooks/useMembers.ts
new file mode 100644
index 0000000..1fd6bf4
--- /dev/null
+++ b/src/app/shared/hooks/useMembers.ts
@@ -0,0 +1,26 @@
+'use client'
+
+import authHttpClient from '@lib/axios'
+import { getTeamId } from '@lib/getTeamId'
+
+export type Member = {
+ id: number
+ email: string
+ nickname: string
+ profileImageUrl: string | null
+ isOwner: boolean | string
+ userId: number
+}
+
+const teamId = getTeamId()
+
+export async function fetchMembers(dashboardId: string): Promise {
+ const { data } = await authHttpClient.get(`/${teamId}/members`, {
+ params: {
+ page: 1,
+ size: 100,
+ dashboardId,
+ },
+ })
+ return data.members ?? data
+}
diff --git a/src/app/shared/hooks/usePagination.ts b/src/app/shared/hooks/usePagination.ts
new file mode 100644
index 0000000..8838112
--- /dev/null
+++ b/src/app/shared/hooks/usePagination.ts
@@ -0,0 +1,33 @@
+import { useMemo, useState } from 'react'
+
+export function usePagination(items: T[], pageSize: number) {
+ const [currentPage, setCurrentPage] = useState(1)
+
+ const totalPages = Math.ceil(items.length / pageSize)
+
+ const startIndex = (currentPage - 1) * pageSize
+
+ const currentItems = useMemo(() => {
+ return items.slice(startIndex, startIndex + pageSize)
+ }, [items, startIndex, pageSize])
+
+ function handlePrev() {
+ if (currentPage > 1) {
+ setCurrentPage((prev) => prev - 1)
+ }
+ }
+
+ function handleNext() {
+ if (currentPage < totalPages) {
+ setCurrentPage((prev) => prev + 1)
+ }
+ }
+
+ return {
+ currentPage,
+ totalPages,
+ currentItems,
+ handlePrev,
+ handleNext,
+ }
+}
diff --git a/src/app/shared/lib/getTeamId.ts b/src/app/shared/lib/getTeamId.ts
new file mode 100644
index 0000000..549543a
--- /dev/null
+++ b/src/app/shared/lib/getTeamId.ts
@@ -0,0 +1,7 @@
+export function getTeamId(): string {
+ const teamId = process.env.NEXT_PUBLIC_TEAM_ID
+ if (!teamId) {
+ throw new Error('NEXT_PUBLIC_TEAM_ID가 환경 변수에 설정되지 않았습니다.')
+ }
+ return teamId
+}
diff --git a/src/app/shared/store/useModalStore.ts b/src/app/shared/store/useModalStore.ts
index 51a7348..7837df3 100644
--- a/src/app/shared/store/useModalStore.ts
+++ b/src/app/shared/store/useModalStore.ts
@@ -4,7 +4,7 @@ import { create } from 'zustand'
import { ModalState } from '@/types/modal'
export const useModalStore = create((set) => ({
- modalType: null,
+ modalType: 'none',
openModal: (type) => set({ modalType: type }),
- closeModal: () => set({ modalType: null }),
+ closeModal: () => set({ modalType: 'none' }),
}))