diff --git a/src/apis/zzal.ts b/src/apis/zzal.ts index 8b12c68e..d0cf7c73 100644 --- a/src/apis/zzal.ts +++ b/src/apis/zzal.ts @@ -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) => { @@ -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({ url: `/v1/image/${imageId}` }); + export const postImageLike = (imageId: number) => http.post({ url: `/v1/image/${imageId}/like`, diff --git a/src/components/ImageDetailModal/ButtonWithIcon.tsx b/src/components/ImageDetailModal/ButtonWithIcon.tsx index 078b8c0d..bfd10424 100644 --- a/src/components/ImageDetailModal/ButtonWithIcon.tsx +++ b/src/components/ImageDetailModal/ButtonWithIcon.tsx @@ -2,6 +2,7 @@ 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 { Icon: LucideIcon; @@ -9,9 +10,17 @@ interface Props extends ButtonHTMLAttributes { 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 ( ); diff --git a/src/components/ImageDetailModal/TagSlider.tsx b/src/components/ImageDetailModal/TagSlider.tsx index 8589ba39..6b827a42 100644 --- a/src/components/ImageDetailModal/TagSlider.tsx +++ b/src/components/ImageDetailModal/TagSlider.tsx @@ -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; @@ -71,15 +71,13 @@ const TagSlider = ({ tags, textSize = "xs", className, onClick }: Props) => { > - - {tags.map(({ name, id }) => ( - + {tags.map(({ tagName, tagId }) => ( + ))} - - - + + ); +}; + +const ImageDetailModal = ({ isOpen, onClose }: Props) => { + return ( + + + + + ); }; diff --git a/src/components/common/Spinner.tsx b/src/components/common/Spinner.tsx new file mode 100644 index 00000000..53ced727 --- /dev/null +++ b/src/components/common/Spinner.tsx @@ -0,0 +1,9 @@ +const Spinner = () => { + return ( +
+ +
+ ); +}; + +export default Spinner; diff --git a/src/components/common/ZzalCard.tsx b/src/components/common/ZzalCard.tsx index 8ce02554..32855020 100644 --- a/src/components/common/ZzalCard.tsx +++ b/src/components/common/ZzalCard.tsx @@ -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"; diff --git a/src/hooks/api/zzal/useGetZzalDetails.ts b/src/hooks/api/zzal/useGetZzalDetails.ts new file mode 100644 index 00000000..9b66f704 --- /dev/null +++ b/src/hooks/api/zzal/useGetZzalDetails.ts @@ -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; diff --git a/src/types/tag.ts b/src/types/tag.ts index 7ec382fd..16fd2aef 100644 --- a/src/types/tag.ts +++ b/src/types/tag.ts @@ -3,10 +3,3 @@ export interface Tag { tagName: string; count: number; } - -export interface TagDetail { - id: number; - name: string; - splitName: string; - createdAt: string; -} diff --git a/src/types/zzal.dto.ts b/src/types/zzal.dto.ts index f4467d4b..63c31c37 100644 --- a/src/types/zzal.dto.ts +++ b/src/types/zzal.dto.ts @@ -1,3 +1,5 @@ +import { Tag } from "./tag"; + export interface GetMyLikedZzalsResponse { imageId: number; path: string; @@ -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[]; +} diff --git a/src/utils/copyZzal.ts b/src/utils/copyZzal.ts deleted file mode 100644 index 2a29125c..00000000 --- a/src/utils/copyZzal.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { toast } from "react-toastify"; -import axios from "axios"; - -const copyToClipboard = (pngBlob: Blob) => { - try { - navigator.clipboard.write([ - new ClipboardItem({ - [pngBlob.type]: pngBlob, - }), - ]); - toast.success("성공적으로 복사되었습니다."); - } catch (error) { - toast.error("복사에 실패했습니다."); - console.error(error); - } -}; - -const copyImageToClipboard = (imageBlob: Blob) => { - const image = new Image(); - image.src = URL.createObjectURL(imageBlob); - - image.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = image.naturalWidth; - canvas.height = image.naturalHeight; - - const ctx = canvas.getContext("2d"); - - if (!ctx) { - console.error("Canvas context를 생성할 수 없습니다."); - return; - } - - ctx.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight); - canvas.toBlob((blob) => { - if (blob) { - copyToClipboard(blob); - return; - } - - console.error("Canvas to Blob 변환 실패"); - }, "image/png"); - }; - - image.onerror = () => { - console.error("이미지 로딩 실패"); - }; -}; - -export const copyZzal = async (imageUrl: string) => { - try { - const { data: imageBlob } = await axios.get(imageUrl, { - headers: { "Cache-Control": "no-cache" }, - responseType: "blob", - }); - - copyImageToClipboard(imageBlob); - } catch (error) { - toast.error("복사에 실패했습니다."); - console.error(error); - } -}; diff --git a/src/utils/zzalUtils.ts b/src/utils/zzalUtils.ts index 83cd0135..5e5c4b6a 100644 --- a/src/utils/zzalUtils.ts +++ b/src/utils/zzalUtils.ts @@ -6,25 +6,84 @@ interface DownloadZzalParameters { imageTitle: string; } -export const downloadZzal = ({ imageUrl, imageTitle }: DownloadZzalParameters) => { - axios - .get(imageUrl, { +export const downloadZzal = async ({ imageUrl, imageTitle }: DownloadZzalParameters) => { + try { + const { data: imageBlob } = await axios.get(imageUrl, { responseType: "blob", headers: { "Cache-Control": "no-cache" }, - }) - .then((response) => { - const file = new File([response.data], "file.jpg", { type: "image/jpeg" }); - const downloadLink = document.createElement("a"); - - downloadLink.href = URL.createObjectURL(file); - downloadLink.download = `${imageTitle}.jpg`; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - toast.success("성공적으로 다운로드 되었습니다"); - }) - .catch((error) => { - toast.error("다운로드에 실패했습니다."); - console.error("파일 다운로드 중 오류가 발생했습니다:", error); }); + + const file = new File([imageBlob], "file.jpg", { type: "image/jpeg" }); + const downloadLink = document.createElement("a"); + + downloadLink.href = URL.createObjectURL(file); + downloadLink.download = `${imageTitle}.jpg`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + toast.success("성공적으로 다운로드 되었습니다"); + } catch (error) { + toast.error("다운로드에 실패했습니다."); + console.error("파일 다운로드 중 오류가 발생했습니다:", error); + } +}; + +const copyToClipboard = (pngBlob: Blob) => { + try { + navigator.clipboard.write([ + new ClipboardItem({ + [pngBlob.type]: pngBlob, + }), + ]); + toast.success("성공적으로 복사되었습니다."); + } catch (error) { + toast.error("복사에 실패했습니다."); + console.error(error); + } +}; + +const copyImageToClipboard = (imageBlob: Blob) => { + const image = new Image(); + image.src = URL.createObjectURL(imageBlob); + + image.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + + const canvasContext = canvas.getContext("2d"); + + if (!canvasContext) { + console.error("Canvas context를 생성할 수 없습니다."); + return; + } + + canvasContext.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight); + canvas.toBlob((blob) => { + if (blob) { + copyToClipboard(blob); + return; + } + + console.error("Canvas to Blob 변환 실패"); + }, "image/png"); + }; + + image.onerror = () => { + console.error("이미지 로딩 실패"); + }; +}; + +export const copyZzal = async (imageUrl: string) => { + try { + const { data: imageBlob } = await axios.get(imageUrl, { + responseType: "blob", + headers: { "Cache-Control": "no-cache" }, + }); + + copyImageToClipboard(imageBlob); + } catch (error) { + toast.error("복사에 실패했습니다."); + console.error(error); + } };