Skip to content

Commit

Permalink
[#69] Feat: 짤 상세 모달 get api 구현 및 적용 (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
hyeonjinan096 authored Mar 11, 2024
1 parent a350660 commit 2d1ba1b
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 167 deletions.
9 changes: 6 additions & 3 deletions src/apis/zzal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import http from "@/apis/core";
import { GetMyLikedZzalsResponse } from "@/types/zzal.dto";
import { GetZzalResponse } from "@/types/zzal.dto";
import { GetMyLikedZzalsResponse, GetZzalDetailsResponse, GetZzalResponse } from "@/types/zzal.dto";
import http from "./core";

import { PAGINATION_LIMIT } from "@/constants/api";

export const deleteMyZzal = (imageId: number) => {
Expand All @@ -12,6 +12,9 @@ export const getMyLikedZzals = (offset: number) =>
url: `/v1/image/like?page=${offset}&size=${PAGINATION_LIMIT}`,
});

export const getZzalDetails = (imageId: number) =>
http.get<GetZzalDetailsResponse>({ url: `/v1/image/${imageId}` });

export const postImageLike = (imageId: number) =>
http.post<GetZzalResponse>({
url: `/v1/image/${imageId}/like`,
Expand Down
14 changes: 12 additions & 2 deletions src/components/ImageDetailModal/ButtonWithIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@ import { ButtonHTMLAttributes } from "react";
import { ReactNode } from "react";
import { LucideIcon } from "lucide-react";
import { cn } from "@/utils/tailwind";
import Spinner from "../common/Spinner";

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
Icon: LucideIcon;
iconLabel: string;
children: ReactNode;
onClick: () => void;
isDisabled?: boolean;
isLoading?: boolean;
}

const ButtonWithIcon = ({ Icon, iconLabel, children, onClick, isDisabled = false }: Props) => {
const ButtonWithIcon = ({
Icon,
iconLabel,
children,
onClick,
isDisabled = false,
isLoading = false,
}: Props) => {
return (
<button
onClick={onClick}
Expand All @@ -21,7 +30,8 @@ const ButtonWithIcon = ({ Icon, iconLabel, children, onClick, isDisabled = false
"cursor-pointer": !isDisabled,
})}
>
<Icon aria-label={iconLabel} />
{!isLoading && <Icon aria-label={iconLabel} />}
{isLoading && <Spinner />}
<span className="mt-1 hidden text-xs sm:flex">{children}</span>
</button>
);
Expand Down
12 changes: 5 additions & 7 deletions src/components/ImageDetailModal/TagSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Navigation } from "swiper/modules";
import SwiperCore from "swiper";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/utils/tailwind";
import { TagDetail } from "@/types/tag";
import { Tag } from "@/types/tag";
import TagBadge from "../common/TagBadge";

interface Props {
tags: TagDetail[];
tags: Tag[];
textSize?: string;
className?: string;
onClick?: () => void;
Expand Down Expand Up @@ -71,15 +71,13 @@ const TagSlider = ({ tags, textSize = "xs", className, onClick }: Props) => {
>
<ChevronLeft strokeWidth={1.5} />
</button>

{tags.map(({ name, id }) => (
<SwiperSlide key={id} className="w-fit cursor-pointer text-center text-text-primary">
{tags.map(({ tagName, tagId }) => (
<SwiperSlide key={tagId} className="w-fit cursor-pointer text-center text-text-primary">
<button onClick={onClick}>
<TagBadge content={name} className={`bg-primary px-2 py-1 text-${textSize}`} />
<TagBadge content={tagName} className={`bg-primary px-2 py-1 text-${textSize}`} />
</button>
</SwiperSlide>
))}

<button
className={cn(arrowButtonClasses, "right-0 bg-gradient-to-l pl-7pxr", {
hidden: !showNextButton,
Expand Down
133 changes: 66 additions & 67 deletions src/components/ImageDetailModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useState } from "react";
import { useState, Fragment, Suspense } from "react";
import { toast } from "react-toastify";
import { useOverlay } from "@toss/use-overlay";
import { Heart, Copy, FolderDown, SendHorizontal, Siren, Trash2, Hash } from "lucide-react";
import { useOverlay } from "@toss/use-overlay";
import { cn } from "@/utils/tailwind";
import { copyZzal, downloadZzal } from "@/utils/zzalUtils";
import { debounce } from "@/utils/debounce";
import ReportConfirmModal from "../ReportConfirmModal";
import ButtonWithIcon from "./ButtonWithIcon";
import TagSlider from "./TagSlider";
import Modal from "@/components/common/modals/Modal";
import useGetZzalDetails from "@/hooks/api/zzal/useGetZzalDetails";
import usePostReportZzal from "@/hooks/api/zzal/usePostReportZzal";
import useDeleteMyZzal from "@/hooks/api/zzal/useDeleteMyZzal";

Expand All @@ -15,62 +18,21 @@ interface Props {
onClose: () => void;
}

const imageDetails = {
imageId: 1,
uploadUserId: 123,
imgUrl:
"https://zzalmyu-bucket.s3.ap-northeast-2.amazonaws.com/upload/keroro9073%40gmail.comtemp_image1626418983561547350.jpg",
imageLikeYn: true,
tags: [
{
id: 0,
name: "일이삼사오육칠팔구십",
splitName: "o",
createdAt: "2024-03-01T05:59:48.528Z",
},
{
id: 1,
name: "일이삼사오육칠팔구십",
splitName: "o",
createdAt: "2024-03-01T05:59:48.528Z",
},
{
id: 2,
name: "일이삼사오육칠팔구십",
splitName: "o",
createdAt: "2024-03-01T05:59:48.528Z",
},
{ id: 3, name: "음식", splitName: "o", createdAt: "2024-03-01T05:59:48.528Z" },
{ id: 4, name: "여행", splitName: "o", createdAt: "2024-03-01T05:59:48.528Z" },
{ id: 5, name: "무한도전", splitName: "o", createdAt: "2024-03-01T05:59:48.528Z" },
{ id: 6, name: "운동", splitName: "o", createdAt: "2024-03-01T05:59:48.528Z" },
],
imageTitle: "안유진",
};
const IMAGEID = 70;
//TODO: [2024.03.06] 실제 IMAGEID 받기

const ImageDetailModal = ({ isOpen, onClose }: Props) => {
const ImageDetailModalContent = () => {
const [isTagNavigatorOpen, setIsTagNavigatorOpen] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const { zzalDetails } = useGetZzalDetails(IMAGEID);
const { reportZzal } = usePostReportZzal();
const { deleteMyZzal } = useDeleteMyZzal();
const [isTagNavigatorOpen, setIsTagNavigatorOpen] = useState(false);
const { imageId, imageLikeYn, imgUrl, tags, imageTitle, uploadUserId } = imageDetails;
const [isLiked, setIsLiked] = useState(imageLikeYn);
const reportConfirmOverlay = useOverlay();
const isUploader = uploadUserId === 123;
{
/*TODO: [2024.03.01] 추후 실제 사용자 아이디와 비교하기 */
}

const toggleTagNavigator = () => {
setIsTagNavigatorOpen(!isTagNavigatorOpen);
};

const handleClickLike = () => {
setIsLiked((prevLiked) => !prevLiked);
};
const { isLiked, imageUrl, tags, imageTitle, uploadUserId, imageId } = zzalDetails;

const handleDownloadZzal = () => {};

const handleSendToChat = () => {};
const isUploader = uploadUserId === 19;
//TODO: [2024.03.01] 추후 실제 사용자 아이디와 비교하기

const handleClickReportCompeleteButton = (imageId: number) => () => {
reportZzal(imageId, {
Expand All @@ -93,39 +55,65 @@ const ImageDetailModal = ({ isOpen, onClose }: Props) => {
));
};

const handleClickDeleteButton = () => {
const handleClickDeleteButton = debounce(() => {
setIsDeleting(true);

deleteMyZzal(imageId, {
onSuccess: () => {
toast.success("사진이 삭제되었습니다.");
},
onError: () => {
toast.error("사진 삭제에 실패했습니다.");
},
onSettled: () => {
setIsDeleting(false);
},
}); // TODO: [2024-03-05] 모달 클릭 시 URL이 변경되도록 구현 후, 이미지 삭제 성공 시 이전 페이지로 이동하는 navigate 추가 필요
};
}, 500);

const handleCopyZzal = async () => {};
const handleClickDownloadButton = debounce(async () => {
setIsDownloading(true);

{
/*TODO: [2024.03.05] 해당 handler함수 로직 추가하기*/
}
await downloadZzal({
imageUrl,
imageTitle,
});

setIsDownloading(false);
}, 500);

const handleClickCopyButton = debounce(() => {
copyZzal(imageUrl);
}, 500);

const handleClickLikeButton = () => {};

const handleClickSendButton = () => {};

//TODO: [2024.03.05] 해당 handler함수 로직 추가하기

const toggleTagNavigator = () => {
setIsTagNavigatorOpen(!isTagNavigatorOpen);
};

return (
<Modal isOpen={isOpen} onClose={onClose} size="sm">
<Fragment>
<div className="relative flex w-full justify-center">
<div className="z-30 flex h-90pxr w-full justify-center bg-background">
<div className=" flex flex-grow items-center justify-between space-x-4 bg-background px-50pxr py-10pxr">
<ButtonWithIcon
Icon={FolderDown}
iconLabel="다운로드"
children="다운로드"
onClick={handleDownloadZzal}
onClick={handleClickDownloadButton}
isLoading={isDownloading}
isDisabled={isDownloading}
/>
<ButtonWithIcon
Icon={SendHorizontal}
iconLabel="채팅에 전송하기"
children="채팅에 전송하기"
onClick={handleSendToChat}
onClick={handleClickSendButton}
/>
<button
onClick={toggleTagNavigator}
Expand Down Expand Up @@ -153,7 +141,8 @@ const ImageDetailModal = ({ isOpen, onClose }: Props) => {
Icon={Trash2}
iconLabel="삭제하기"
children="삭제하기"
isDisabled={!isUploader}
isDisabled={!isUploader || isDeleting}
isLoading={isDeleting}
onClick={handleClickDeleteButton}
/>
</div>
Expand All @@ -168,13 +157,13 @@ const ImageDetailModal = ({ isOpen, onClose }: Props) => {
</div>
</div>
<div className=" max-h-500pxr overflow-auto">
<img src={imgUrl} alt={imageTitle} className="w-full" />
<img src={imageUrl} alt={imageTitle} className="w-full" />
</div>
<div className="fixed bottom-0 right-0 flex flex-col space-y-4 p-25pxr hover:text-gray-300">
<button onClick={handleCopyZzal}>
<button onClick={handleClickCopyButton}>
<Copy color="white" size={30} aria-label="복사하기" />
</button>
<button onClick={handleClickLike}>
<button onClick={handleClickLikeButton}>
<Heart
color="white"
size={30}
Expand All @@ -183,7 +172,17 @@ const ImageDetailModal = ({ isOpen, onClose }: Props) => {
/>
</button>
</div>
</Modal>
</Fragment>
);
};

const ImageDetailModal = ({ isOpen, onClose }: Props) => {
return (
<Suspense fallback={"...pending"}>
<Modal isOpen={isOpen} onClose={onClose} size="sm">
<ImageDetailModalContent />
</Modal>
</Suspense>
);
};

Expand Down
9 changes: 9 additions & 0 deletions src/components/common/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Spinner = () => {
return (
<div className="h-6 w-6">
<span className="loading loading-spinner loading-xs" />
</div>
);
};

export default Spinner;
2 changes: 1 addition & 1 deletion src/components/common/ZzalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toast } from "react-toastify";
import { Heart, SendHorizontal, Copy } from "lucide-react";
import { useSetAtom } from "jotai";
import { cn } from "@/utils/tailwind";
import { copyZzal } from "@/utils/copyZzal";
import { copyZzal } from "@/utils/zzalUtils";
import { ZzalType } from "@/types/queryKey";
import { useAddImageLike } from "@/hooks/api/zzal/useAddImageLike";
import { $setMessagePreview } from "@/store/chat";
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/api/zzal/useGetZzalDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { getZzalDetails } from "@/apis/zzal";

const useGetZzalDetails = (imageId: number) => {
const { data: zzalDetails, ...rest } = useSuspenseQuery({
queryKey: ["zzalDetails", imageId],
queryFn: () => getZzalDetails(imageId),
select: (data) => ({
isLiked: data.imageLikeYn,
imageUrl: data.imgUrl,
...data,
}),
});
return { zzalDetails, ...rest };
};

export default useGetZzalDetails;
7 changes: 0 additions & 7 deletions src/types/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,3 @@ export interface Tag {
tagName: string;
count: number;
}

export interface TagDetail {
id: number;
name: string;
splitName: string;
createdAt: string;
}
10 changes: 10 additions & 0 deletions src/types/zzal.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Tag } from "./tag";

export interface GetMyLikedZzalsResponse {
imageId: number;
path: string;
Expand All @@ -15,3 +17,11 @@ export interface GetZzalPagesResponse {
path: string;
imageLikeYn: boolean;
}
export interface GetZzalDetailsResponse {
imageId: number;
imageTitle: string;
uploadUserId: number;
imgUrl: string;
imageLikeYn: boolean;
tags: Tag[];
}
Loading

0 comments on commit 2d1ba1b

Please sign in to comment.