-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#69] Feat: 짤 상세 모달 get api 구현 및 적용 #108
Changes from all commits
620260a
4e53bb2
b221816
939e17a
742c6d7
f7b6642
dd6523b
eb4ebda
2d79037
32d0438
4bdf28b
fa3cee9
0d634bc
e18c380
c5432b7
e45ae3e
eace9bf
2a64ba5
cfdba17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
|
||
|
@@ -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, { | ||
|
@@ -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} | ||
|
@@ -153,7 +141,8 @@ const ImageDetailModal = ({ isOpen, onClose }: Props) => { | |
Icon={Trash2} | ||
iconLabel="삭제하기" | ||
children="삭제하기" | ||
isDisabled={!isUploader} | ||
isDisabled={!isUploader || isDeleting} | ||
isLoading={isDeleting} | ||
onClick={handleClickDeleteButton} | ||
/> | ||
</div> | ||
|
@@ -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} | ||
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴포넌트를 쪼개 주신 이유가 있으신가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useSuspenseQuery를 사용하기 위해 컴포넌트 상위에서 Suspense로 감싸줘야했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suspense 때문에 불필요한 컴포넌트 분리가 생겼는데, 이 부분이 좀 걸리네요ㅠ 저도 한 번 고민해보겠습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이미지 상세 모달의 스켈레톤이나 로딩 화면을 보여줄건지, 보여준다면 어떻게 보여줄건지가 중요해질 거 같은데 현진님 생각하신 방향 혹시 있으신가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 처음엔 짤이 있는 페이지에서 로딩 화면 보여주다가 데이터가 다 오면 모달을 띄어주는 걸 생각했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 따로 페이지가 아닌 모달이라 조금 까다롭긴 하네요ㅠ스켈레톤 구현할때 다시 논의해보면 좋을 것 같습니다! |
||
</Suspense> | ||
); | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const Spinner = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. spinner의 사이즈를 조절할 수 있게 daisyUI에서 제공하는 spinner 컴포넌트 사이즈 토큰을 props로 전달받으면 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 관련해서 재민님과 논의해봤는데 현재로는 스피너가 사용되는부분이 짤 상세모달 버튼밖에 없어서 |
||
return ( | ||
<div className="h-6 w-6"> | ||
<span className="loading loading-spinner loading-xs" /> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Spinner; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현진님 궁금증 하나 남겨봅니다 😀
debounce를 써서 더블클릭 등을 했을 때 여러 번 이벤트가 발생하는 것을 방지해줄 수 있군요! 배워갑니다 👍
상세 페이지에 있는 신고, 삭제 버튼에도 debounce를 걸어주는게 좋을까요? 아니면 불필요할까요?
삭제 시에도 debounce를 걸어주는 것이 좋다는 글을 하나 봐서 여쭤봅니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 신고하기는 모달이 바로 나와서 괜찮을 것 같은데 삭제시에는 debounce를 걸어주시면 좋을 것 같습니당👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메인에서 pull해서 삭제 함수에 debounce 적용했습니다~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HY219 삭제 시에 debounce를 걸어주는게 왜 좋은지 여쭤봐도 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
얼핏 봤던 글이였는데 제가 잘못 알았던 것 같습니다.
찾아보니까 아래와 같은 글도 있네요!
현진님 수정해주셨는데 번거롭게 해드렸네요ㅜ!
아직 언제 debounce를 사용해주는지 파악이 안된 것 같아요
https://thewavelet.tistory.com/65
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 그러면 삭제시 디바운스를 제거하고 요청 중에 click하지 못하도록 변경하면 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 그렇게 해주실 수 있으면 저는 좋은 방법이라고 생각합니다! 😀