Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/app/(team)/_components/TeamProfileForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '',
Expand All @@ -29,13 +30,15 @@ export default function TeamProfileForm({
const [preview, setPreview] = useState(initialPreview);
const [file, setFile] = useState<File>();
const [nameError, setNameError] = useState(false);
const [lengthError, setLengthError] = useState(false);
const [imageError, setImageError] = useState('');

useEffect(() => {
setName(initialName);
setPreview(initialPreview);
setFile(undefined);
setNameError(false);
setLengthError(false);
setImageError('');
}, [initialName, initialPreview]);

Expand Down Expand Up @@ -70,13 +73,24 @@ export default function TeamProfileForm({

const removeImage = preview === '' && !file;

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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;
}
Expand All @@ -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 (
<div className="text-md-regular tablet:w-[460px] tablet:text-lg-regular flex w-[343px] flex-col items-center">
Expand Down Expand Up @@ -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"
Expand All @@ -160,6 +172,11 @@ export default function TeamProfileForm({
이미 존재하는 이름입니다.
</p>
)}
{lengthError && (
<p className="text-md-medium mt-2 text-red-500">
팀 이름은 최대 {MAX_NAME_LENGTH}자까지 가능합니다.
</p>
)}
</div>

<div className="mb-6 w-full">
Expand Down
2 changes: 1 addition & 1 deletion src/app/(user)/mypage/_components/AccountUpdateButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const AccountUpdateButton = ({ name, image }: AccountUpdateButtonProps) => {
return patchUser({ body });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
queryClient.invalidateQueries({ queryKey: ['currentUser'] });
toast.success('개인 정보가 수정되었습니다.');
},
onError: () => {
Expand Down
37 changes: 26 additions & 11 deletions src/components/common/Header/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand All @@ -38,7 +42,28 @@ export default function SideMenu({
</div>

<ul className="header-scroll flex h-[700px] flex-col gap-6 overflow-y-auto overscroll-contain text-sm font-normal">
{memberships.map(({ group, role }) => (
<li>
<Link
href={ROUTES.BOARDS}
className="hover:text-green-700"
onClick={onClose}
>
자유게시판
</Link>
</li>
<li>
<Link
href={ROUTES.TEAM_ADD}
className="hover:text-green-700"
onClick={onClose}
>
팀 생성하기
</Link>
</li>
{!!sortedMemberships.length && (
<li className="mx-auto text-slate-300">- 팀 목록 -</li>
)}
{sortedMemberships.map(({ group, role }) => (
<li
key={group.id}
className="flex items-center justify-between"
Expand Down Expand Up @@ -66,16 +91,6 @@ export default function SideMenu({
)}
</li>
))}

<li>
<Link
href={ROUTES.BOARDS}
className="hover:text-green-700"
onClick={onClose}
>
자유게시판
</Link>
</li>
</ul>
</div>
</>
Expand Down
190 changes: 102 additions & 88 deletions src/components/common/Header/TeamMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand All @@ -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 (
<div className="relative ml-8 flex gap-3">
<Link href={baseLink} className="text-md font-medium hover:text-gray-700">
{label}
</Link>

<button
type="button"
className={`z-50 cursor-pointer hover:text-gray-700 ${
isOpen ? 'rotate-180' : ''
} transition-transform`}
onClick={() => setIsOpen((o) => !o)}
>
<IconRenderer name="CheckIcon" className="hover:text-gray-700" />
</button>
<div className="ml-8 flex items-center gap-3">
{teamId !== undefined && selectedGroup ? (
<Link
href={ROUTES.TEAM(selectedGroup.id)}
className="text-md max-w-60 truncate font-medium hover:text-gray-700"
onClick={dropdpwnClose}
>
{selectedGroup.name}
</Link>
) : (
<button
type="button"
className="text-md font-medium hover:text-gray-700"
onClick={dropdownOpen}
>
팀 목록
</button>
)}
<div className="relative flex items-center">
<button
type="button"
className={`z-50 cursor-pointer hover:text-gray-700 ${
isOpen ? 'rotate-180' : ''
} transition-transform`}
onClick={dropdownOpen}
>
<IconRenderer name="CheckIcon" className="hover:text-gray-700" />
</button>

{isOpen && (
<>
<button
type="button"
className="fixed inset-0 z-40 cursor-default"
onClick={close}
/>
{isOpen && (
<>
<button
type="button"
className="fixed inset-0 z-40 cursor-default"
onClick={dropdpwnClose}
/>

<div className="absolute top-[45px] left-[-140px] z-50 flex w-[240px] flex-col gap-4 rounded-xl border border-slate-50/10 bg-slate-800 p-4">
<div className="header-scroll flex max-h-[350px] flex-col gap-4 overflow-y-auto">
{memberships.map(({ group, role }) => (
<div
key={group.id}
className="flex items-center gap-x-3 rounded-md px-2 py-2"
>
<Link
href={ROUTES.TEAM(group.id)}
className="py flex flex-1 items-center gap-x-3 rounded-md p-1 transition-colors hover:bg-slate-700"
onClick={() => {
onSelect(group);
setIsOpen(false);
}}
<div className="absolute top-12 right-0 z-50 mt-2 flex w-[240px] flex-col gap-4 rounded-xl border border-slate-50/10 bg-slate-800 p-4">
<div className="header-scroll flex max-h-[350px] flex-col gap-4 overflow-y-auto">
{sortedMemberships.map(({ group, role }) => (
<div
key={group.id}
className="flex items-center gap-x-3 rounded-md px-2 py-2"
>
<div className="relative h-8 w-8">
{group.image ? (
<Image
src={`${group.image}`}
alt={group.name}
fill
// 임시조치 - 나중에 도메인 추가 예정
unoptimized
className="rounded-sm object-cover"
/>
) : (
<IconRenderer name="ImgIcon" size={32} />
)}
</div>
<span className="text-sm whitespace-nowrap">
{group.name}
</span>
</Link>

{role === 'ADMIN' && (
<Link
href={ROUTES.TEAM_EDIT(group.id)}
onClick={() => setIsOpen(false)}
href={ROUTES.TEAM(group.id)}
className="py flex min-w-0 flex-1 items-center gap-x-3 rounded-md p-1 transition-colors hover:bg-slate-700"
onClick={() => {
onSelect(group);
setIsOpen(false);
}}
>
<IconRenderer
name="EditIcon"
size={20}
className="cursor-pointer hover:text-green-700"
/>
<div className="relative h-8 w-8">
{group.image ? (
<Image
src={`${group.image}`}
alt={group.name}
fill
// 임시조치 - 나중에 도메인 추가 예정
unoptimized
className="rounded-sm object-cover"
/>
) : (
<IconRenderer name="ImgIcon" size={32} />
)}
</div>
<span className="truncate text-sm">{group.name}</span>
</Link>
)}
</div>
))}
</div>

<Link href={ROUTES.TEAM_ADD}>
<Button
variant="floating"
styleType="transparent"
radius="sm"
size="lg"
className="!w-full"
startIcon="plus"
onClick={close}
>
팀 추가하기
</Button>
</Link>
</div>
</>
)}
{role === 'ADMIN' && (
<Link
href={ROUTES.TEAM_EDIT(group.id)}
className="ml-2 flex-shrink-0"
onClick={() => setIsOpen(false)}
>
<IconRenderer
name="EditIcon"
size={20}
className="ml-auto flex-shrink-0 cursor-pointer hover:text-green-700"
/>
</Link>
)}
</div>
))}
</div>

<Link href={ROUTES.TEAM_ADD}>
<Button
variant="floating"
styleType="transparent"
radius="sm"
size="lg"
className="!w-full"
startIcon="plus"
onClick={dropdpwnClose}
>
팀 추가하기
</Button>
</Link>
</div>
</>
)}
</div>
</div>
);
}
Loading