Skip to content
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

[#80] Feat: 짤카드 레이아웃 변경, 복사 아이콘 추가 및 각각의 버튼 기능 구현 #107

Merged
merged 28 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b76f60f
Feat: 이미지 좋아요 추가 fetching 함구 구현
choi-ik Mar 2, 2024
daa368b
Feat: 이미지 좋아요 추가 mutation 함수 구현
choi-ik Mar 2, 2024
5f3671f
Refactor: 이미지 내의 버튼 위치 변경 및 클립보드 복사, 좋아요 fetching 기능 추가
choi-ik Mar 2, 2024
6ae6559
Feat: 보내기 버튼 클릭시 preview 이미지 생성 기능 추가
choi-ik Mar 2, 2024
6586d06
Merge: main 브랜치 merge
choi-ik Mar 6, 2024
a941049
Feat: 홈 화면에 나타낼 짤 카드 type 선언
choi-ik Mar 6, 2024
df3caf9
Feat: 이미지 좋아요, 이미지 좋아요 취소 api 구현
choi-ik Mar 6, 2024
d43ea94
Refactor: 이미지 복사 util 함수 cors 에러 해결
choi-ik Mar 6, 2024
e7146ca
Feat: 이미지 좋아요 mutation 함수 구현
choi-ik Mar 6, 2024
76bacea
Feat: 이미지 좋아요 취소 mutation 함수 구현
choi-ik Mar 6, 2024
184a3d4
Refactor: 짤카드 공통 컴포넌트 레이아웃 변경, 복사 아이콘 및 기능 추가, 좋아요 버튼 기능 추가, 보내기 버튼 기…
choi-ik Mar 6, 2024
6860648
Chore: routeTree 자동 변경
choi-ik Mar 6, 2024
c9f785d
Refactor: 짤 좋아요, 좋아요 취소 무한 스크롤과 쿼리키에 대해 고려하지 않았던 로직 수정
choi-ik Mar 6, 2024
c401e1a
Modify: 짤 관련 response 타입 수정 및 추가
choi-ik Mar 6, 2024
3012049
Refactor: 짤카드 쿼리 키 prop 추가
choi-ik Mar 6, 2024
846882c
Refactor: 각 mutation error 콘솔 추가
choi-ik Mar 6, 2024
0ce8f9c
Refactor: 짤 카드 prop과 useContext의 상태값 변경
choi-ik Mar 6, 2024
f72ee50
Rename: 짤카드 api 네이밍 변경
choi-ik Mar 7, 2024
dd0132e
Rename: import한 좋아요 관련 api 네이밍 수정
choi-ik Mar 7, 2024
03f9086
Refactor: 짤카드 템플릿 리터럴 제거
choi-ik Mar 7, 2024
d9d38cd
Cleanup: 사용되지 않는 리다이렉트 페이지 routeTree에서 제거
choi-ik Mar 7, 2024
80265b9
Chore: 짤카드 관련 쿼리키 타입 추가
choi-ik Mar 7, 2024
fc2d565
Refactor: 좋아요 클릭 된 짤의 인덱스를 찾는 util 함수 구현 후 add,removeImageLike.ts에 적용
choi-ik Mar 7, 2024
4ef2531
Refactor: 좋아요 누른 짤 위치를 찾기 위한 util 함수에 isLiked 매개변수 추가
choi-ik Mar 7, 2024
95745da
Refactor: getQueryData의 타입 단언 제거
choi-ik Mar 8, 2024
5924a6b
Rename: 좋아요 추가/취소 query의 onError 파라미터 네이밍 수정
choi-ik Mar 8, 2024
100ff02
Refactor: 낙관적 업데이트를 위한 캐시 데이터 수정 로직 선언적으로 변경
choi-ik Mar 8, 2024
45e6790
Cleanup: 사용되지 않는 util 함수 삭제
choi-ik Mar 8, 2024
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
19 changes: 18 additions & 1 deletion src/apis/zzal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import http from "@/apis/core";
import { GetMyLikedZzalsResponse } from "@/types/zzal.dto";
import http from "./core";
import { GetZzalResponse } from "@/types/zzal.dto";
import { PAGINATION_LIMIT } from "@/constants/api";

export const deleteMyZzal = (imageId: number) => {
Expand All @@ -10,3 +11,19 @@ export const getMyLikedZzals = (offset: number) =>
http.get<GetMyLikedZzalsResponse>({
url: `/v1/image/like?page=${offset}&size=${PAGINATION_LIMIT}`,
});

export const postImageLike = (imageId: number) =>
http.post<GetZzalResponse>({
url: `/v1/image/${imageId}/like`,
params: {
imageId,
},
});

export const deleteImageLike = (imageId: number) =>
http.post<GetZzalResponse>({
url: `/v1/image/${imageId}/like/cancel`,
params: {
imageId,
},
});
105 changes: 82 additions & 23 deletions src/components/common/ZzalCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { ReactNode } from "react";
import { Heart, SendHorizontal } from "lucide-react";
import { ReactNode, createContext, useContext } from "react";
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 { ZzalType } from "@/types/queryKey";
import { useAddImageLike } from "@/hooks/api/zzal/useAddImageLike";
import { $setMessagePreview } from "@/store/chat";
import { useRemoveImageLike } from "@/hooks/api/zzal/useRemoveImageLike";

interface ZzalCardProps {
children?: ReactNode;
Expand All @@ -11,6 +18,14 @@ interface ZzalCardProps {
className?: string;
}

interface ZzalCardContextType {
src: string;
}

const ZzalCardContext = createContext<ZzalCardContextType>({
src: "",
});

const ZzalCard = ({
children,
src,
Expand All @@ -20,32 +35,55 @@ const ZzalCard = ({
className,
}: ZzalCardProps) => {
return (
<div className={cn(`group relative w-${width} rounded-lg bg-base-100 shadow-xl`, className)}>
<div className="button-container absolute right-2 top-1 z-10 w-fit opacity-0 transition-opacity duration-500 ease-in-out group-hover:opacity-100">
{children}
<ZzalCardContext.Provider value={{ src }}>
<div className={cn(`group relative w-${width} rounded-lg bg-base-100 shadow-xl`, className)}>
<div className="button-container absolute bottom-2 right-2 z-10 flex w-fit gap-1.5 opacity-0 transition-opacity duration-500 ease-in-out group-hover:opacity-100">
{children}
</div>
<figure
className={cn(
"h-fit",
hasAnimation ? "transition duration-300 ease-in-out hover:brightness-75" : "none",
)}
>
<img src={src} alt={alt} className="h-full w-full rounded-lg object-cover" />
</figure>
</div>
<figure
className={cn(
"h-fit",
`${hasAnimation ? "transition duration-300 ease-in-out hover:brightness-75" : "none"}`,
)}
>
<img src={src} alt={alt} className="h-full w-full rounded-lg object-cover" />
</figure>
</div>
</ZzalCardContext.Provider>
);
};

interface LikeButtonProps {
onClick: () => void;
imageId: number;
isLiked: boolean;
imageIndex: number;
queryKey: ZzalType;
}

const LikeButton = ({ onClick, isLiked }: LikeButtonProps) => {
const LikeButton = ({ imageId, isLiked, imageIndex, queryKey }: LikeButtonProps) => {
const { addImageLike } = useAddImageLike(imageIndex, queryKey);
const { removeImageLike } = useRemoveImageLike(imageIndex, queryKey);

const handleClickImageLike = () => {
if (!isLiked) {
addImageLike(imageId, {
onError: () =>
toast.error("좋아요 요청이 실패하였습니다 다시 시도해주세요.", { autoClose: 1500 }),
});

return;
}

removeImageLike(imageId, {
onError: () =>
toast.error("좋아요 취소에 실패하였습니다 다시 시도해주세요.", { autoClose: 1500 }),
});
};

return (
<button
className="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-white"
onClick={onClick}
onClick={handleClickImageLike}
>
<Heart
aria-label="좋아요"
Expand All @@ -57,22 +95,43 @@ const LikeButton = ({ onClick, isLiked }: LikeButtonProps) => {
);
};

interface SendButtonProps {
onClick: () => void;
}
const SendButton = () => {
const { src } = useContext(ZzalCardContext);
const setPreviewImage = useSetAtom($setMessagePreview);

const handleClickSendImageSrc = () => {
setPreviewImage(src);
};

return (
<button
className="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-primary"
onClick={handleClickSendImageSrc}
>
<SendHorizontal aria-label="채팅창 보내기" size={20} fill="white" />
</button>
);
};

const CopyButton = () => {
const { src } = useContext(ZzalCardContext);

const handleClickCopytoClipboard = () => {
copyZzal(src);
};

const SendButton = ({ onClick }: SendButtonProps) => {
return (
<button
className="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-primary"
onClick={onClick}
onClick={handleClickCopytoClipboard}
>
<SendHorizontal aria-label="보내기" size={20} fill="white" />
<Copy aria-label="이미지 복사" size={20} stroke="white" />
</button>
);
};

ZzalCard.SendButton = SendButton;
ZzalCard.LikeButton = LikeButton;
ZzalCard.CopyButton = CopyButton;

export default ZzalCard;
36 changes: 36 additions & 0 deletions src/hooks/api/zzal/useAddImageLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQueryClient, useMutation } from "@tanstack/react-query";
import { postImageLike } from "@/apis/zzal";
import { GetZzalResponse } from "@/types/zzal.dto";
import { ZzalType } from "@/types/queryKey";

export const useAddImageLike = (imageIndex: number, zzalKey: ZzalType) => {
const queryClient = useQueryClient();

const { mutate, ...rest } = useMutation({
mutationFn: (imageId: number) => postImageLike(imageId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: [zzalKey] });

const oldData = queryClient.getQueryData<GetZzalResponse>([zzalKey]);

if (!oldData) return;

const updatedData = {
pageParams: [...oldData.pageParams],
pages: [...oldData.pages.flatMap((page) => page)],
};

updatedData.pages[imageIndex].imageLikeYn = true;

queryClient.setQueryData([zzalKey], updatedData);

return { oldData };
},
onError: (error, _zzalId, context) => {
console.error(error);
queryClient.setQueryData([zzalKey], context?.oldData);
},
});

return { addImageLike: mutate, ...rest };
};
36 changes: 36 additions & 0 deletions src/hooks/api/zzal/useRemoveImageLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQueryClient, useMutation } from "@tanstack/react-query";
import { deleteImageLike } from "@/apis/zzal";
import { GetZzalResponse } from "@/types/zzal.dto";
import { ZzalType } from "@/types/queryKey";

export const useRemoveImageLike = (imageIndex: number, zzalKey: ZzalType) => {
const queryClient = useQueryClient();

const { mutate, ...rest } = useMutation({
mutationFn: (imageId: number) => deleteImageLike(imageId),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: [zzalKey] });

const oldData = queryClient.getQueryData<GetZzalResponse>([zzalKey]);

if (!oldData) return;

const updatedData = {
pageParams: [...oldData.pageParams],
pages: [...oldData.pages.flatMap((page) => page)],
};

updatedData.pages[imageIndex].imageLikeYn = false;

queryClient.setQueryData([zzalKey], updatedData);

return { oldData };
},
onError: (error, _zzalId, context) => {
console.error(error);
queryClient.setQueryData([zzalKey], context?.oldData);
},
});

return { removeImageLike: mutate, ...rest };
};
1 change: 1 addition & 0 deletions src/types/queryKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ZzalType = "likedZzals" | "uploadedZzals" | "mainZzals";
12 changes: 12 additions & 0 deletions src/types/zzal.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@ export interface GetMyLikedZzalsResponse {
path: string;
title: string;
}

export interface GetZzalResponse {
pageParams: number[];
pages: GetZzalPagesResponse[][];
}

export interface GetZzalPagesResponse {
imageId: number;
title: string;
path: string;
imageLikeYn: boolean;
}
5 changes: 4 additions & 1 deletion src/utils/copyZzal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ const copyImageToClipboard = (imageBlob: Blob) => {

export const copyZzal = async (imageUrl: string) => {
try {
const { data: imageBlob } = await axios.get<Blob>(imageUrl, { responseType: "blob" });
const { data: imageBlob } = await axios.get<Blob>(imageUrl, {
headers: { "Cache-Control": "no-cache" },
responseType: "blob",
});

copyImageToClipboard(imageBlob);
} catch (error) {
Expand Down
Loading