diff --git a/src/app/(team)/_components/TeamProfileForm/index.tsx b/src/app/(team)/_components/TeamProfileForm/index.tsx index c60137f4..037a5760 100644 --- a/src/app/(team)/_components/TeamProfileForm/index.tsx +++ b/src/app/(team)/_components/TeamProfileForm/index.tsx @@ -17,6 +17,7 @@ export interface TeamProfileFormProps { } const MAX_FILE_SIZE = 4.2 * 1024 * 1024; +const MAX_NAME_LENGTH = 30; export default function TeamProfileForm({ initialName = '', @@ -29,6 +30,7 @@ export default function TeamProfileForm({ const [preview, setPreview] = useState(initialPreview); const [file, setFile] = useState(); const [nameError, setNameError] = useState(false); + const [lengthError, setLengthError] = useState(false); const [imageError, setImageError] = useState(''); useEffect(() => { @@ -36,6 +38,7 @@ export default function TeamProfileForm({ setPreview(initialPreview); setFile(undefined); setNameError(false); + setLengthError(false); setImageError(''); }, [initialName, initialPreview]); @@ -70,13 +73,24 @@ export default function TeamProfileForm({ const removeImage = preview === '' && !file; + const handleNameChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setName(value); + setNameError(false); + setLengthError(value.length > MAX_NAME_LENGTH); + }; + const handleSubmit = async () => { let hasErr = false; + const trimmed = name.trim(); if (existingNames.includes(name.trim())) { setNameError(true); hasErr = true; } - + if (trimmed.length > MAX_NAME_LENGTH) { + setLengthError(true); + hasErr = true; + } if (imageError) { hasErr = true; } @@ -85,7 +99,8 @@ export default function TeamProfileForm({ await onSubmit({ name: name.trim(), file, removeImage }); }; - const isDisabled = !name.trim() || Boolean(imageError); + const isDisabled = + !name.trim() || Boolean(imageError) || Boolean(lengthError); return (
@@ -146,10 +161,7 @@ export default function TeamProfileForm({ value={name} autoComplete="off" isInvalid={nameError} - onChange={(e) => { - setName(e.target.value); - setNameError(false); - }} + onChange={handleNameChange} onKeyDown={handleKeyDown} titleClassName="mb-3" containerClassName=" h-11 tablet:h-12 bg-slate-800" @@ -160,6 +172,11 @@ export default function TeamProfileForm({ 이미 존재하는 이름입니다.

)} + {lengthError && ( +

+ 팀 이름은 최대 {MAX_NAME_LENGTH}자까지 가능합니다. +

+ )}
diff --git a/src/app/(user)/mypage/_components/AccountUpdateButton.tsx b/src/app/(user)/mypage/_components/AccountUpdateButton.tsx index 1e6d0aeb..48c95ad7 100644 --- a/src/app/(user)/mypage/_components/AccountUpdateButton.tsx +++ b/src/app/(user)/mypage/_components/AccountUpdateButton.tsx @@ -34,7 +34,7 @@ const AccountUpdateButton = ({ name, image }: AccountUpdateButtonProps) => { return patchUser({ body }); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['user'] }); + queryClient.invalidateQueries({ queryKey: ['currentUser'] }); toast.success('개인 정보가 수정되었습니다.'); }, onError: () => { diff --git a/src/components/common/Header/SideMenu.tsx b/src/components/common/Header/SideMenu.tsx index 2279f08e..60ac1215 100644 --- a/src/components/common/Header/SideMenu.tsx +++ b/src/components/common/Header/SideMenu.tsx @@ -14,6 +14,10 @@ export default function SideMenu({ onClose: () => void; memberships: UserMembershipResponse[]; }) { + const sortedMemberships = [...memberships].sort( + (a, b) => a.group.id - b.group.id + ); + return ( <> {isSideMenuOpen && ( @@ -38,7 +42,28 @@ export default function SideMenu({
diff --git a/src/components/common/Header/TeamMenu.tsx b/src/components/common/Header/TeamMenu.tsx index 6a13634b..77acbf08 100644 --- a/src/components/common/Header/TeamMenu.tsx +++ b/src/components/common/Header/TeamMenu.tsx @@ -25,10 +25,14 @@ export default function TeamMenu({ const teamId = teamIdParam ? Number(teamIdParam) : undefined; const [isOpen, setIsOpen] = useState(false); + const sortedMemberships = [...memberships].sort( + (a, b) => a.group.id - b.group.id + ); useEffect(() => { if (teamId == null || isNaN(teamId)) return; if (selectedGroup?.id === teamId) return; + const found = memberships.find( (membership) => membership.group.id === teamId ); @@ -37,103 +41,113 @@ export default function TeamMenu({ } }, [teamId, memberships, selectedGroup, onSelect]); - const label = teamId != null ? selectedGroup?.name : '팀 목록'; - const close = () => setIsOpen(false); - - const baseLink = selectedGroup - ? ROUTES.TEAM(selectedGroup.id) - : ROUTES.TEAM_NO; + const dropdpwnClose = () => setIsOpen(false); + const dropdownOpen = () => setIsOpen((prev) => !prev); return ( -
- - {label} - - - +
+ {teamId !== undefined && selectedGroup ? ( + + {selectedGroup.name} + + ) : ( + + )} +
+ - {isOpen && ( - <> - - -
- - )} + {role === 'ADMIN' && ( + setIsOpen(false)} + > + + + )} +
+ ))} +
+ + + + + + + )} + ); } diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index d79970ba..d948aefa 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import Link from 'next/link'; import { toast } from 'react-toastify'; import Cookies from 'js-cookie'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import IconRenderer from '@/components/common/Icons/IconRenderer'; import Logo from './Logo'; import TeamMenu from './TeamMenu'; @@ -15,6 +15,7 @@ import { useMemberships } from '@/hooks/useMemberships'; import { getUser } from '@/lib/apis/user'; import { UserResponse } from '@/lib/apis/user/type'; import { ROUTES } from '@/constants/routes'; +import { usePathname } from 'next/navigation'; export default function Header() { const isLogin = useAuth(); @@ -22,6 +23,14 @@ export default function Header() { useMemberships(isLogin); const [isSideMenuOpen, setSideMenuOpen] = useState(false); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.invalidateQueries({ queryKey: ['memberships'] }); + queryClient.invalidateQueries({ queryKey: ['currentUser'] }); + }, [pathname, queryClient]); + const logoHref = selectedGroup ? ROUTES.TEAM(selectedGroup.id) : ROUTES.TEAM_NO; @@ -37,9 +46,19 @@ export default function Header() { }); const handleLogout = () => { - Cookies.remove('accessToken', { path: '/' }); - Cookies.remove('refreshToken', { path: '/' }); - Cookies.remove('userId', { path: '/' }); + Cookies.remove('accessToken', { + path: '/', + secure: true, + sameSite: 'Strict', + }); + + Cookies.remove('refreshToken', { + path: '/', + secure: true, + sameSite: 'Strict', + }); + Cookies.remove('userId', { path: '/', secure: true, sameSite: 'Strict' }); + location.href = '/'; toast.success('로그아웃 되었습니다'); }; diff --git a/src/hooks/useGroupQueries.ts b/src/hooks/useGroupQueries.ts index 8f42f02a..85213f66 100644 --- a/src/hooks/useGroupQueries.ts +++ b/src/hooks/useGroupQueries.ts @@ -1,3 +1,5 @@ +'use client'; + import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getGroupById, @@ -8,6 +10,11 @@ import { import uploadImage from '@/lib/apis/uploadImage'; import type { GroupBody, GroupResponse } from '@/lib/apis/group/type'; +type UpdateGroupVariables = GroupBody & { + file?: File; + removeImage?: boolean; +}; + export function useGroup(teamId: number) { return useQuery({ queryKey: ['group', teamId], @@ -31,43 +38,39 @@ export function useCreateGroup() { }); }, onSuccess: () => { - qc.invalidateQueries({ queryKey: ['userGroups'] }); + qc.invalidateQueries({ queryKey: ['memberships'] }); }, }); } export function useUpdateGroup(teamId: number) { const qc = useQueryClient(); - return useMutation({ - mutationFn: async ( - data: GroupBody & { file?: File; removeImage?: boolean } - ): Promise => { - const body: { name: string; image?: string | null } = { name: data.name }; + return useMutation({ + mutationFn: async (data) => { + const body: { name: string; image?: string | null } = { name: data.name }; if (data.file) { const url = await uploadImage(data.file); body.image = url; } else if (data.removeImage) { body.image = null; } - return patchGroupById({ - groupId: teamId, - body, - }); + return patchGroupById({ groupId: teamId, body }); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['group', teamId] }); - qc.invalidateQueries({ queryKey: ['userGroups'] }); + qc.invalidateQueries({ + queryKey: ['memberships'], + }); }, }); } - export function useDeleteGroup(teamId: number) { const qc = useQueryClient(); return useMutation({ mutationFn: () => deleteGroupById({ groupId: teamId }), onSuccess: () => { - qc.invalidateQueries({ queryKey: ['userGroups'] }); + qc.invalidateQueries({ queryKey: ['memberships'] }); }, }); } diff --git a/src/hooks/useMemberships.ts b/src/hooks/useMemberships.ts index eeaeaa34..3b5b86ba 100644 --- a/src/hooks/useMemberships.ts +++ b/src/hooks/useMemberships.ts @@ -2,8 +2,8 @@ import { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; -import fetcher from '@/lib/client/fetcher.client'; import { UserMembershipResponse } from '@/lib/apis/user/type'; +import { getUserMemberships } from '@/lib/apis/user'; export function useMemberships(isLogin: boolean) { const { data: memberships = [] } = useQuery< @@ -13,10 +13,7 @@ export function useMemberships(isLogin: boolean) { >({ queryKey: ['memberships'], queryFn: async () => { - const res = await fetcher({ - url: '/user/memberships', - method: 'GET', - }); + const res = await getUserMemberships({ tag: ['memberships'] }); return res ?? []; }, enabled: isLogin, @@ -28,11 +25,12 @@ export function useMemberships(isLogin: boolean) { useEffect(() => { if (memberships.length > 0) { - setSelectedGroup((prev) => - prev && memberships.some((m) => m.group.id === prev.id) - ? prev - : memberships[0].group - ); + setSelectedGroup((prev) => { + const newGroup = memberships.find( + (m) => m.group.id === prev?.id + )?.group; + return newGroup ?? memberships[0].group; + }); } else { setSelectedGroup(null); }