Skip to content
Merged
File renamed without changes
22 changes: 22 additions & 0 deletions src/components/Button/NavButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Image from 'next/image';

interface NavButtonProps {
direction: 'left' | 'right';
onClick: () => void;
isDisable?: boolean;
}

export default function NavButton({ direction, onClick, isDisable }: NavButtonProps) {
return (
<button
className={`btn-white rounded-none ${direction === 'left' ? 'rounded-s-[4px]' : 'rounded-e-[4px]'} size-9 md:size-10`}
onClick={onClick}
type='button'
disabled={isDisable}
>
<div className={`${direction === 'left' ? 'rotate-180' : ''} relative h-[12px] w-[8px]`}>
<Image src={'/icons/arrow-white.svg'} alt={`arrow-${direction}`} fill />
</div>
</button>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트로 빼주셨네요 감사합니다! 🫡👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 따로 언급해써야 했는데 감빡했네요!!
민재님 이미 만드신 부분은 제가 같이 수정할 예정이고, 다른 곳에서 페이지네이션이나 버튼 필요하면 사용해주세요~~

22 changes: 22 additions & 0 deletions src/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import NavButton from '../Button/NavButton';

interface PaginationProps {
currentChunk: number;
totalPage: number;
hideText?: boolean;
onNextClick: () => void;
onPrevClick: () => void;
}

export default function Pagination({ currentChunk, totalPage, hideText, onNextClick, onPrevClick }: PaginationProps) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wjsdncl hideText={true}로 줄 경우 글자 안보이게 할 수 있습니다!
근데 민재님은 모바일에서 디자인 변화도 생겨서 이 컴포넌트 사용하기 어려울 수 있겠네요..

return (
<div className='flex items-center justify-end'>
<span className={`pr-3 text-xs text-black-33 md:pr-4 md:text-sm ${hideText ? 'hidden' : ''}`}>
{totalPage} 페이지 중 {currentChunk}
</span>

<NavButton direction='left' onClick={() => onPrevClick()} isDisable={currentChunk === 1} />
<NavButton direction='right' onClick={() => onNextClick()} isDisable={currentChunk === totalPage} />
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이것도 빼주셨네요 고생하셨습니다! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

span 부분 default로 보이게 하고, 설정에 따라 hidden으로 할 수 있게 추가해 놓겠습니다!
사이드바에는 span이 없어서!!

17 changes: 17 additions & 0 deletions src/containers/dashboard/edit/InvitedMemberItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import CancelButton from '@/components/Button/CancelButton';

interface InvitedMemberProps {
email: string;
onCancelClick: () => void;
}

export default function InvitedMemberItem({ email, onCancelClick }: InvitedMemberProps) {
return (
<div className='flex items-center justify-between'>
{email}
<CancelButton className='text-sm' onClick={onCancelClick}>
취소
</CancelButton>
</div>
);
}
28 changes: 28 additions & 0 deletions src/containers/dashboard/edit/InvitedMemberList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import InvitedMemberItem from './InvitedMemberItem';

import { Invitation } from '@/types/Invitation.interface';

interface Props {
invitations: Invitation[];
onCancelClick: (id: number) => void;
}

export default function InvitedMemberList({ invitations, onCancelClick }: Props) {
const lastItem = invitations.at(-1);

return (
<ol className='flex flex-col gap-3 md:gap-4'>
{invitations.slice(0, invitations.length - 1).map(({ id, invitee }) => (
<li key={id} className='flex flex-col gap-3 md:gap-4'>
<InvitedMemberItem email={invitee.email} onCancelClick={() => onCancelClick(id)} />
<div className='h-0 w-full border border-gray-ee' />
</li>
))}
{lastItem && (
<li key={lastItem.id}>
<InvitedMemberItem email={lastItem.invitee.email} onCancelClick={() => onCancelClick(lastItem.id)} />
</li>
)}
</ol>
);
}
96 changes: 96 additions & 0 deletions src/containers/dashboard/edit/InvitedMembersSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useQueryClient } from '@tanstack/react-query';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useState } from 'react';

import InvitedMemberList from './InvitedMemberList';

import Pagination from '@/components/Pagination';
import useDeleteData from '@/hooks/useDeleteData';
import useFetchData from '@/hooks/useFetchData';
import { deleteInvitation } from '@/services/deleteService';
import { getDashboardInvitations } from '@/services/getService';
import { CancelInvitationInput } from '@/types/delete/CancelInvitation.interface';
import { DashboardInvitationsResponse } from '@/types/Invitation.interface';

export default function InvitedMembersSection() {
const router = useRouter();
const { id } = router.query;
const [currentChunk, setCurrentChunk] = useState(1);

const handleSuccess = () => {
if (data?.invitations.length === 1 && currentChunk > 1) {
setCurrentChunk((prev) => prev - 1);
}
queryClient.invalidateQueries({ queryKey: ['invitations', id] });
};

const { mutate } = useDeleteData<CancelInvitationInput>({ mutationFn: deleteInvitation, handleSuccess });
const queryClient = useQueryClient();

const { data, error } = useFetchData<DashboardInvitationsResponse>(['invitations', id, currentChunk], () =>
getDashboardInvitations(Number(id), currentChunk, 5),
);
const totalPage = data ? Math.max(1, Math.ceil(data.totalCount / 5)) : 1;

const handleNext = () => {
if (currentChunk < totalPage) {
setCurrentChunk((prev) => prev + 1);
}
};
const handlePrev = () => {
if (currentChunk > 1) {
setCurrentChunk((prev) => prev - 1);
}
};

const handleInviteClick = () => {
// TODO: 모달 연결
alert('초대 모달');
};

const handleCancelInvitation = (invitationId: number) => {
const handleDelete = async () => {
if (!id) return;
await mutate({ dashboardId: String(id), invitationId });
};

handleDelete();
};

return (
<section className='section h-[395px] pb-4 md:h-[477px] md:pb-5'>
<header className='mb-4 mt-6 flex items-center justify-between md:my-7'>
<h2 className='section-title'>초대 내역</h2>
<nav className='align-center relative gap-3'>
<Pagination
currentChunk={currentChunk}
totalPage={totalPage}
onNextClick={handleNext}
onPrevClick={handlePrev}
/>
<button
className='btn-violet absolute right-0 top-12 flex gap-1.5 rounded-md px-3 text-xs md:static md:px-4'
type='button'
onClick={handleInviteClick}
>
<div className='relative my-[7px] size-[14px] md:my-2 md:size-4'>
<Image src='/icons/plusbox-white.svg' alt='초대 아이콘' fill priority />
</div>
<p className='mb-1.5 mt-[7px] md:mb-[7px] md:mt-2'>초대하기</p>
</button>
</nav>
</header>
<main className='text-sm md:text-base'>
<h3 className='mb-[29px] text-gray-9f md:mb-6'>이메일</h3>
{data ? (
<InvitedMemberList invitations={data.invitations} onCancelClick={handleCancelInvitation} />
) : error ? (
<p>{`에러가 발생했습니다. \n${error.message}`}</p>
) : (
<p>로딩중...</p>
)}
</main>
</section>
);
}
39 changes: 5 additions & 34 deletions src/containers/mydashboard/DashboardList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';

import Pagination from '@/components/Pagination';
import useFetchData from '@/hooks/useFetchData';
import { getDashboardsList } from '@/services/getService';
import { DashboardsResponse } from '@/types/Dashboard.interface';
Expand All @@ -25,24 +26,19 @@ export default function DashboardList() {
}

const handleNext = () => {
const nextChunk = currentChunk + 1;

if (nextChunk <= totalPage) {
if (currentChunk < totalPage) {
setCurrentChunk((prev) => prev + 1);
}
};

const handlePrev = () => {
const prevChunk = currentChunk - 1;

if (prevChunk >= 1) {
if (currentChunk > 1) {
setCurrentChunk((prev) => prev - 1);
}
};

return (
<section className='w-max'>
<ul className='grid grid-rows-1 gap-3 font-semibold text-black-33 md:min-h-[216px] md:grid-cols-2 md:grid-rows-3 lg:min-h-[140px] lg:grid-cols-3 lg:grid-rows-2'>
<ul className='grid grid-rows-1 gap-3 pb-3 font-semibold text-black-33 md:min-h-[216px] md:grid-cols-2 md:grid-rows-3 lg:min-h-[140px] lg:grid-cols-3 lg:grid-rows-2'>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wjsdncl 기존에 작업하신 내 대시보드 목록에도 페이지네이션 적용했습니다!
원래 버튼에 pt-3이 있었는데, 리스트에 pb-3을 주는 것으로 바꿨습니다!

image

<li className='h-16 w-64 rounded-lg border border-gray-d9 bg-white md:w-60 lg:w-80'>
<button className='btn-violet-light size-full gap-4'>
새로운 대시보드
Expand All @@ -63,32 +59,7 @@ export default function DashboardList() {
))}
</ul>

<div className='flex items-center justify-end pt-3'>
<span className='pr-4 text-sm text-black-33'>
{totalPage} 페이지 중 {currentChunk}
</span>

<NavButton direction='left' onClick={handlePrev} isDisable={currentChunk === 1} />
<NavButton direction='right' onClick={handleNext} isDisable={currentChunk === totalPage} />
</div>
<Pagination currentChunk={currentChunk} totalPage={totalPage} onNextClick={handleNext} onPrevClick={handlePrev} />
</section>
);
}

interface NavButtonProps {
direction: 'left' | 'right';
onClick: () => void;
isDisable?: boolean;
}

const NavButton = ({ direction, onClick, isDisable }: NavButtonProps) => (
<button
className={`btn-white rounded-none ${direction === 'left' ? 'rounded-s-[4px]' : 'rounded-e-[4px]'} size-10`}
onClick={onClick}
disabled={isDisable}
>
<div className={`${direction === 'left' ? 'rotate-180' : ''} relative h-[12px] w-[8px]`}>
<Image src={'/icons/arrow-white.svg'} alt={`arrow-${direction}`} fill />
</div>
</button>
);
36 changes: 36 additions & 0 deletions src/hooks/useDeleteData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MutationFunction, useMutation } from '@tanstack/react-query';
import { AxiosError } from 'axios';

interface UseDeleteProps<T> {
mutationFn: MutationFunction<unknown, T>;
handleSuccess?: () => void;
handleError?: () => void;
}

const useDeleteData = <T>({ mutationFn, handleSuccess, handleError }: UseDeleteProps<T>) => {
return useMutation<unknown, unknown, T, unknown>({
mutationFn,
onSuccess: () => {
// NOTE: success하고 나서 하고싶은 작업 있으면 prop으로 넘겨주세요.
if (handleSuccess) {
handleSuccess();
}
},
onError: (error: unknown) => {
// NOTE: 기본적인 에러처리 이외에 다른 처리를 하고 싶으면 prop으로 넘겨주세요.
if (handleError) {
handleError();
} else {
// NOTE: 에러 처리는 일관되게 서버 메세지 있는 경우 보여주고, 아니면 로그 출력하도록 했습니다.
if (error instanceof AxiosError) {
alert(error.response?.data.message);
} else {
alert('실패했습니다.');
console.log(error);
}
}
},
});
};

export default useDeleteData;
15 changes: 11 additions & 4 deletions src/pages/dashboard/[id]/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const DashboardEditPage: React.FC = () => {
return <div>DashboardEdit</div>;
};
import InvitedMembersSection from '@/containers/dashboard/edit/InvitedMembersSection';

export default DashboardEditPage;
export default function DashboardEditPage() {
return (
<div className='flex flex-col gap-4 px-3 py-4 text-black-33 md:p-5'>
{/* 돌아가기 */}
{/* 대시보드 수정 */}
{/* 구성원 목록 */}
<InvitedMembersSection />
</div>
);
}
13 changes: 10 additions & 3 deletions src/services/deleteService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import instance from './axios';

import { CancelInvitationInput } from '@/types/delete/CancelInvitation.interface';

// 대시보드 삭제
export const deleteDashboard = async (dashboardId: number) => {
return await instance.delete(`/dashboards/${dashboardId}`);
};

// 컬럼 삭제
export const deleteColumn = async (columnId: number) => {
return await instance.delete(`/columns/${columnId}`);
};

// 대시보드 삭제
export const deleteDashboard = async (dashboardId: number) => {
return await instance.delete(`/dashboards/${dashboardId}`);
// 대시보드 초대 취소
export const deleteInvitation = async ({ dashboardId, invitationId }: CancelInvitationInput) => {
return await instance.delete(`/dashboards/${dashboardId}/invitations/${invitationId}`);
};
9 changes: 9 additions & 0 deletions src/services/getService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ export const getMembersList = async (
return await instance.get(`/members?page=${page}&size=${size}&dashboardId=${dashboardId}`);
};

// 대시보드 초대 목록 조회
export const getDashboardInvitations = async (
dashboardId: number,
page: number = 1, // 기본값 1
size: number = 5, // 기본값 5
) => {
return await instance.get(`/dashboards/${dashboardId}/invitations?page=${page}&size=${size}`);
};

// 내가 받은 초대 목록 조회
export const getInvitationsList = async (size: number = 10, cursorId?: number, title?: string) => {
const params = new URLSearchParams();
Expand Down
2 changes: 1 addition & 1 deletion src/services/putService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const putProfile = async (formData: UpdateProfileForm) => {
export const putPassword = async (formData: UpdatePasswordForm) => {
return await instance.put<User>(`/auth/password`, formData);
};

// 컬럼 수정
export const putColumn = async (columnId: number, formData: { title: string }) => {
return await instance.put(`/columns/${columnId}`, formData);
Expand Down
4 changes: 4 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
@apply align-center bg-gray-f5 rounded-md hover:bg-gray-ee active:bg-gray-d9;
}

.section {
@apply w-full max-w-[620px] rounded-lg bg-white px-5 md:px-7;
}

.section-title {
@apply text-xl font-bold md:text-2xl;
}
Expand Down
5 changes: 5 additions & 0 deletions src/types/Invitation.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export interface InvitationsResponse {
cursorId: number;
}

export interface DashboardInvitationsResponse {
invitations: Invitation[];
totalCount: number;
}

export interface Invitation {
id: number;
inviter: {
Expand Down
4 changes: 4 additions & 0 deletions src/types/delete/CancelInvitation.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CancelInvitationInput {
invitationId: number;
dashboardId: string;
}