Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const nextConfig: NextConfig = {
"cdsassets.apple.com",
"encrypted-tbn3.gstatic.com",
"blogs.nvidia.co.kr",
"panda-prisma.onrender.com",
],
},
};
Expand Down
5 changes: 5 additions & 0 deletions public/assets/ic_delete_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/assets/ic_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 12 additions & 27 deletions src/app/freeboard/[articleId]/core/hooks/useArticleDetailQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,18 @@ import {
PatchArticleApiProps,
DeleteArticleApiProps,
} from "../service/articleDetailService";
import { Article } from "@/shared/type";
import { Article, DeleteCommentResponse } from "@/shared/type";
import { articleKeys } from "@/shared/utils/queryKeys";

export const useGetArticleDetail = (articleId: string) => {
if (typeof articleId === "string") {
const { data, isLoading } = useQuery<Article>({
queryKey: articleKeys.detail(articleId),
queryFn: () => getArticleDetailAPI({ articleId: articleId }),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});

return {
data: {
id: data?.id ?? "",
title: data?.title ?? "",
content: data?.content ?? "",
image: data?.image ?? "",
favoritesCount: data?.favoritesCount ?? 0,
createdAt: data?.createdAt ?? "",
updatedAt: data?.updatedAt ?? "",
},
isLoading,
};
}
const { data, isLoading } = useQuery<Article>({
queryKey: articleKeys.detail(articleId),
queryFn: () => getArticleDetailAPI({ articleId }),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});

return {
data: null,
isLoading: false,
};
return { data, isLoading };
};

export const useUpdateArticle = () => {
Expand All @@ -57,7 +39,7 @@ export const useUpdateArticle = () => {
export const useDeleteArticle = () => {
const queryClient = useQueryClient();

return useMutation<void, Error, DeleteArticleApiProps>({
return useMutation<DeleteCommentResponse, Error, DeleteArticleApiProps>({
mutationFn: (params: DeleteArticleApiProps) => deleteArticleAPI(params),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
Expand All @@ -67,5 +49,8 @@ export const useDeleteArticle = () => {
queryKey: articleKeys.all,
});
},
onError: (error) => {
window.alert("게시글 삭제에 실패했습니다.");
},
});
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AxiosResponse } from "axios";
import { instance } from "@/shared/utils/APIs/axiosInstance";
import { Article } from "@/shared/type";
import { myInstance } from "@/shared/service/myApi/myInstance";
import { Article, DeleteCommentResponse } from "@/shared/type";

export interface GetArticleDetailApiProps {
articleId: string;
Expand All @@ -12,7 +12,7 @@ export const getArticleDetailAPI = async ({
articleId,
}: GetArticleDetailApiProps): Promise<Article> => {
try {
const response: AxiosResponse<Article> = await instance.get(
const response: AxiosResponse<Article> = await myInstance.get(
`/articles/${articleId}`
);
// console.log("getArticleDetail", response.data);
Expand All @@ -27,7 +27,7 @@ export interface PatchArticleApiProps {
articleId: string;
title: string;
content: string;
image?: string;
image?: File;
}

export const patchArticleAPI = async ({
Expand All @@ -37,7 +37,7 @@ export const patchArticleAPI = async ({
image,
}: PatchArticleApiProps): Promise<Article> => {
try {
const response: AxiosResponse<Article> = await instance.patch(
const response: AxiosResponse<Article> = await myInstance.patch(
`/articles/${articleId}`,
{ title, content, image }
);
Expand All @@ -54,9 +54,11 @@ export interface DeleteArticleApiProps {

export const deleteArticleAPI = async ({
articleId,
}: DeleteArticleApiProps): Promise<void> => {
}: DeleteArticleApiProps): Promise<DeleteCommentResponse> => {
try {
await instance.delete(`/articles/${articleId}`);
const response: AxiosResponse<DeleteCommentResponse> =
await myInstance.delete(`/articles/${articleId}`);
return response.data;
} catch (err) {
throw err;
}
Expand Down
67 changes: 66 additions & 1 deletion src/app/freeboard/[articleId]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
useUpdateArticle,
} from "../core/hooks/useArticleDetailQuery";
import { useState, useEffect } from "react";
import { ArticleImgInput } from "../../core/components/ArticleImgInput";

export default function Page() {
const router = useRouter();
Expand All @@ -26,8 +27,12 @@ export default function Page() {
const [formData, setFormData] = useState({
title: "",
content: "",
image: undefined as File | undefined,
imageUrl: undefined as string | undefined,
});

const [showMaxImageError, setShowMaxImageError] = useState(false);

// id가 없으면 early return
if (!id) {
router.push("/freeboard");
Expand All @@ -44,9 +49,11 @@ export default function Page() {
setFormData({
title: articleData.title,
content: articleData.content,
image: undefined,
imageUrl: articleData.image || undefined,
});
}
}, [articleData?.title, articleData?.content]);
}, [articleData]);

const titlePlaceholder = "제목을 입력해주세요";
const contentPlaceholder = "내용을 입력해주세요";
Expand Down Expand Up @@ -76,14 +83,66 @@ export default function Page() {
return !formData.title.trim() || !formData.content.trim();
};

const handleImageInput = () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/*";

fileInput.onchange = (e: Event) => {
const files = (e.target as HTMLInputElement).files;
if (!files || !files[0]) return;

if (files[0].size > 5 * 1024 * 1024) {
window.alert("이미지 크기는 5MB 이하여야 합니다.");
return;
}

const imageUrl = URL.createObjectURL(files[0]);
setFormData((prev) => ({
...prev,
image: files[0],
imageUrl,
}));
};

if (formData.image) {
setShowMaxImageError(true);
setTimeout(() => setShowMaxImageError(false), 3000);
return;
}

fileInput.click();
};

const handleDeleteImage = () => {
setFormData((prev) => {
if (prev.imageUrl && prev.image) {
URL.revokeObjectURL(prev.imageUrl);
}
return {
...prev,
image: undefined,
imageUrl: undefined,
};
});
};

const handleClickUpdateArticle = () => {
if (isFormDisabled()) return;

const formDataObj = new FormData();
formDataObj.append("title", formData.title.trim());
formDataObj.append("content", formData.content.trim());
if (formData.image) {
formDataObj.append("images", formData.image);
}

updateArticle(
{
articleId: id,
title: formData.title.trim(),
content: formData.content.trim(),
image: formData.image,
},
{
onSuccess: () => {
Expand Down Expand Up @@ -174,6 +233,12 @@ export default function Page() {
/>
</FormControl>
</Stack>
<ArticleImgInput
onClickFileInput={handleImageInput}
imageUrl={formData.imageUrl}
onClickDeleteImg={handleDeleteImage}
showMaxImageError={showMaxImageError}
/>
</Stack>
</Stack>
</CommonLayout>
Expand Down
42 changes: 25 additions & 17 deletions src/app/freeboard/[articleId]/feature/ArticleDetails/feature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,27 @@ import { EditEllipsis } from "@/shared/components/EditEllipsis";
import { formatDate } from "@/shared/utils/getFormattedDate";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useArticleFavoriteHook } from "@/app/freeboard/core/hooks/useArticleFavoriteHook";

export const ArticleDetails = ({ articleId }: { articleId: string }) => {
const router = useRouter();
const { data, isLoading } = useGetArticleDetail(articleId);
const { mutate: deleteArticle } = useDeleteArticle();
const { isFavorite, handleToggleFavorite } = useArticleFavoriteHook({
articleId,
initialFavorite: data?.isLiked ?? false,
});

const handleUpdate = () => {
router.push(`/freeboard/${articleId}/edit`);
};

const handleDelete = () => {
if (window.confirm("정말 삭제하시겠습니까?")) {
deleteArticle({ articleId });
router.push("/freeboard");
}
};

if (isLoading || !data) {
return (
Expand All @@ -31,23 +47,11 @@ export const ArticleDetails = ({ articleId }: { articleId: string }) => {
);
}

const { title, content, favoritesCount, createdAt } = data;
//FIXME: 아직 user 정보가 없어서 임시 닉네임, 프로필 이미지 디폴트로 설정
const nickname = "총명한판다";
const { title, content, likeCount, createdAt, ownerNickname } = data;
const nickname = ownerNickname;
const profileImg = "/assets/default_profile.png";
const formattedDate = formatDate(createdAt);

const handleUpdate = () => {
router.push(`/freeboard/${articleId}/edit`);
};

const handleDelete = () => {
if (window.confirm("정말 삭제하시겠습니까?")) {
deleteArticle({ articleId });
router.push("/freeboard");
}
};

return (
<Stack sx={articleDetailsSx}>
<Stack sx={articleHeaderSx}>
Expand Down Expand Up @@ -88,17 +92,21 @@ export const ArticleDetails = ({ articleId }: { articleId: string }) => {
borderRight: `1px solid ${colorChips.gray200}`,
}}
/>
<Stack sx={favoriteCountSx}>
<Stack sx={favoriteCountSx} onClick={handleToggleFavorite}>
<Image
src="/assets/ic_heart_gray5.svg"
src={
isFavorite
? "/assets/ic_heart_pink.svg"
: "/assets/ic_heart_gray5.svg"
}
alt="favorite"
width={32}
height={32}
style={{ cursor: "pointer" }}
/>
<Typo
className="text16Medium"
content={favoritesCount.toString()}
content={likeCount.toString()}
color={colorChips.gray500}
/>
</Stack>
Expand Down
1 change: 0 additions & 1 deletion src/app/freeboard/[articleId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default function Page() {
? params.articleId[0]
: params.articleId;

//FIXME: 게시글 ID가 없는 경우 404나 알림 띄우는거 추가하면 좋을 듯
if (!articleId) {
return null;
}
Expand Down
Loading