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

Post 작성 서버 연결 #27

Merged
merged 2 commits into from
Nov 11, 2023
Merged
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
130 changes: 112 additions & 18 deletions client/src/app/(protectedRoute)/new-post/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import {
AppBar,
Avatar,
Box,
Button,
ButtonBase,
Container,
Paper,
Expand All @@ -15,25 +15,71 @@ import GoBackIcon from "@/assets/icons/GoBackIcon.svg";
import InputSearchIcon from "@/assets/icons/InputSearchIcon.svg";
import AlcholeSearchIcon from "@/assets/icons/AlcholeSearchIcon.svg";
import { useRouter } from "next/navigation";
import { ChangeEvent, useState } from "react";
import { ChangeEvent, useEffect, useState } from "react";
import { useUserInfoQuery } from "@/queries/auth/useUserInfoQuery";
import axios from "@/libs/axios";
import HOME from "@/const/clientPath";

export default function NewpostPage() {
const router = useRouter();
const [formValue, setFormValue] = useState({
alcoholNo: "",
alcoholFeature: "",
postContent: "",
postType: "BASIC",
positionInfo: "",
tagList: ["string"],
tagList: [] as string[],
});

const changeHadler = ({
target,
}: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormValue((prev) => ({ ...prev, [target.name]: target.value }));
};

const token = localStorage.getItem("accessToken");

const router = useRouter();

const submitHandler = () => {
let userId;
let pk;
axios
.get("/user/me", { headers: { Authorization: token } })
.then((res) => {
userId = res.data.id;
})
.then(() => {
axios
.post(
"/posts",
{ ...formValue },
{ headers: { Authorization: token } }
)
.then(({ data }) => {
pk = data.postNo;
const formData = new FormData();
if (file) {
formData.append("image", file);
}
axios
.post(`/attach/resources/POST/${pk}`, formData, {
headers: {
Authorization: token,
"Content-Type": "multipart/form-data",
},
transformRequest: [
function () {
return formData;
},
],
})
.then(() => {
router.push(HOME);
});
});
});
};
const [userTypedTag, setUserTypedTag] = useState<string>();
const [file, setFile] = useState<File>();

return (
<>
<AppBar position={"static"}>
Expand All @@ -59,35 +105,83 @@ export default function NewpostPage() {
p: 2,
}}
component="form"
onSubmit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<TextField
placeholder="검색어를 입력해주세요"
placeholder="지금 어떤 술을 마시고 있나요?"
autoFocus
name="positionInfo"
InputProps={{
startAdornment: (
<AlcholeSearchIcon style={{ marginRight: "8px" }} />
),
endAdornment: <InputSearchIcon />,
}}
onChange={changeHadler}
/>

<TextField
id="filled-multiline-flexible"
placeholder="입력해주세요"
multiline
name={"postContent"}
onChange={changeHadler}
value={formValue.postContent}
rows={6}
/>

<Box>
<TextField
id="filled-multiline-flexible"
placeholder="입력해주세요"
multiline
name={"postContent"}
onChange={changeHadler}
value={formValue.postContent}
rows={6}
/>
</Box>
<Typography variant="label" sx={{ textAlign: "right" }}>
{formValue.postContent.length} /{" "}
<Typography variant="label" color="primary.main" component="span">
200자
</Typography>
</Typography>
{formValue.tagList.map((tag) => {
return <Typography variant="label">{tag}</Typography>;
})}
<Box>
<TextField
onChange={({ target }) => setUserTypedTag(target.value)}
value={userTypedTag}
></TextField>
<Button
onClick={(e) => {
e.preventDefault();
setFormValue((prev) => {
if (!userTypedTag) return prev;
return { ...prev, tagList: [...prev.tagList, userTypedTag] };
});
setUserTypedTag("");
}}
>
태그 추가
</Button>
</Box>
<Button component="label" variant="contained">
Upload file
<input
id="image"
type="file"
accept="image/*"
name="image"
onChange={(e) => {
if (e.target.files) {
setFile(e.target.files[0]);
let reader = new FileReader();
reader.readAsDataURL(e.target.files[0]);
reader.onloadend = () => {
// 2. 읽기가 완료되면 아래코드가 실행됩니다.
const base64 = reader.result;
};
// setFile(e.target.files[0]);
}
}}
/>
</Button>
<Button type="submit">작성하기</Button>
</Paper>
</Container>
</>
Expand Down
2 changes: 1 addition & 1 deletion client/src/assets/icons/LikeIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 20 additions & 5 deletions client/src/components/post/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import CommentIcon from "@/assets/icons/CommentIcon.svg";
import QuoteIcon from "@/assets/icons/QuoteIcon.svg";
import AlcoleNameTag from "./AlcoleNameTag";
import dayjs from "dayjs";
import useLikePostMutation from "@/queries/post/useLikePostMutation";
import useUnLikePostMutation from "@/queries/post/useUnLikePostMutation";

const PostCard = ({
postAttachUrls,
Expand All @@ -35,9 +37,12 @@ const PostCard = ({
alcoholName,
alcoholType,
commentCount,
likedByme,
}: PostInterface) => {
const openPostDetailPage = useOpenPostDetailPage();
const hasImage = useMemo(() => postAttachUrls.length !== 0, [postAttachUrls]);
const { mutate: likeHandler } = useLikePostMutation();
const { mutate: unLikeHandler } = useUnLikePostMutation();

return (
<Card sx={{ display: "flex", gap: 2, p: 2 }}>
Expand Down Expand Up @@ -82,7 +87,9 @@ const PostCard = ({
<MoreVertOutlined />
</ButtonBase>
</Box>
<AlcoleNameTag alcoholName={alcoholName} alcoholType={alcoholType} />
{alcoholName && (
<AlcoleNameTag alcoholName={alcoholName} alcoholType={alcoholType} />
)}

<CardContent sx={{ px: 0 }}>
{/* Contents */}
Expand All @@ -104,8 +111,8 @@ const PostCard = ({
data-testid="postImg"
component="img"
height="142"
onClick={() => openPostDetailPage(id, id)}
image={postAttachUrls[0]}
onClick={() => openPostDetailPage(id, String(postNo))}
image={postAttachUrls[0].attachUrl}
alt={`${id}의 포스트`}
sx={{ borderRadius: 2, bgcolor: "background.default" }}
/>
Expand All @@ -121,8 +128,16 @@ const PostCard = ({
{commentCount ?? 0}
</Typography>
</ButtonBase>
<ButtonBase data-testid="likeBtn" aria-label="like">
<LikeIcon />
<ButtonBase
data-testid="likeBtn"
aria-label="like"
onClick={() => {
likedByme ? unLikeHandler(postNo) : likeHandler(postNo);
}}
>
<Box style={{ color: likedByme ? "primary.main" : "#d9d9d9" }}>
<LikeIcon />
</Box>
<Typography variant="label">{likeCount ?? 0}</Typography>
</ButtonBase>
<ButtonBase data-testid="QuoteBtn" aria-label="Quote">
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/post/PostCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Image from "next/image";
import NoResult from "@/assets/images/noResult.png";

function PostCardList(props: UseGetPostListQueryInterface) {
const { data, fetchNextPage, isFetchingNextPage, isSuccess, hasNextPage } =
const { data, fetchNextPage, isFetchingNextPage, hasNextPage } =
useGetPostListInfiniteQuery({
...props,
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드 패치에 대해 간단한 코드 리뷰를 도와드리겠습니다. 버그 위험 및 개선 제안을 환영합니다:

  • 해당 부분에서 isSuccess 변수가 사용되지 않는 것 같습니다. 이 변수를 사용하는 목적이 무엇인지 확인해 보세요. 필요하지 않다면 제거할 수 있습니다.
  • UseGetPostListQueryInterface 인터페이스의 내용을 알 수 없기 때문에 해당 인터페이스의 정확성을 확인해야 합니다. 필요에 따라 인터페이스를 수정하거나 업데이트해야 할 수도 있습니다.

그 외에는 코드의 논리적인 문제나 버그 위험이 보이지 않습니다.

Expand Down
1 change: 0 additions & 1 deletion client/src/libs/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const axiosPrivate = axios.create({
"Content-Type": "application/json",
},
withCredentials: true,
baseURL: `${process.env.NEXT_PUBLIC_CLIENT_BASE_URL}/api`
});

/**

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드 패치에 대해 간략한 코드 리뷰를 도와드리겠습니다. 버그 위험과/또는 개선 제안을 환영합니다.

보시면 이 코드는 axiosPrivate라는 변수로 내보내기(export)됩니다. 이 변수는 Axios 인스턴스인 axios.create() 메서드로 생성되었습니다.

여러 옵션 중 하나로, "Content-Type" 헤더를 "application/json"으로 설정하는 headers 객체가 있습니다.

또한, withCredentials 옵션이 true로 설정되어 있습니다. 이는 요청과 관련된 쿠키 및 인증 정보가 포함된 응답을 사용하기 위해 필요한 설정입니다.

하지만 기존에 baseURL이 있었으나 제거되었네요. 이 부분이 삭제된 것이 의도된 동작인지 확실히 알 수는 없지만, 다른 곳에서 사용하여야 하는 API의 기본 URL을 설정해야 할 수 있습니다.

이외의 잠재적인 버그나 개선 사항은 현재 코드로는 파악하기 어렵습니다. 전체적인 프로그램의 구조나 다른 파일들의 연관성을 파악하지 못하므로, 이 코드 조각이 어떻게 사용되고 있는지 알 수 없습니다. 완전한 코드 리뷰를 위해서는 전체 코드 또는 더 많은 정보가 필요합니다.

Expand Down
12 changes: 10 additions & 2 deletions client/src/queries/post/useGetPostListInfiniteQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ export const useGetPostListInfiniteQuery = ({
searchKeyword,
}: UseGetPostListQueryInterface) => {
return useSuspenseInfiniteQuery({
queryKey: ["posts", searchKeyword ?? ""],
queryKey: getPostListInfiniteQueryKey.byKeyword(searchKeyword),

queryFn: async ({ pageParam = 0 }) =>
await getPostListQueryFn({ page: pageParam, size, searchKeyword }),

getNextPageParam: ({
currentPage,
hasNextPage,
}: AugmentedGetPostListResponse) =>
hasNextPage ? currentPage + 1 : undefined,

getPreviousPageParam: ({ currentPage }: AugmentedGetPostListResponse) =>
currentPage > 0 ? currentPage - 1 : undefined,
initialPageParam: 0,
Expand Down Expand Up @@ -63,8 +66,13 @@ export const getPostListQueryFn = async ({
return {
...data,
currentPage: page,
hasNextPage: data.totalCount / (page + 1 * size) > 1,
hasNextPage: data.totalCount / ((page + 1) * size) > 1,
};
};

export const getPostListInfiniteQueryKey = {
all: ["posts"] as const,
byKeyword: (keyword?: string) => ["posts", keyword ?? ""] as const,
};

export default useGetPostListInfiniteQuery;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다음은 제시된 코드 패치에 대한 간단한 코드 리뷰입니다. 버그 위험 또는 개선 제안 사항이 있을 경우 언제든지 알려주세요:

  • useGetPostListInfiniteQuery 함수 내부의 수정된 queryKeygetPostListInfiniteQueryKey.byKeyword 함수를 사용하여 생성됩니다. 이 변경은 검색 키워드에 따라 쿼리 키를 동적으로 생성하도록 합니다.

  • queryFn은 페이지 매개변수와 함께 getPostListQueryFn 함수를 호출하여 포스트 목록을 검색합니다. 해당 함수의 반환 형식은 AugmentedGetPostListResponse로 가져옵니다.

  • getNextPageParamgetPreviousPageParam 콜백 함수는 현재 페이지 및 페이지를 지속할 수 있는지 여부에 따라 다음 및 이전 페이지 매개변수를 반환합니다.

  • 수정된 getPostListQueryFn은 페이징 결과를 반환하기 전에 hasNextPage를 계산하는 방식을 변경합니다. 수정된 수식은 더 정확한 결과를 제공합니다.

  • getPostListInfiniteQueryKey 객체는 모든 포스트 및 특정 키워드(옵셔널)로 쿼리 키를 정의하는 데 사용됩니다.

위의 상기 내용을 고려하면 주어진 코드 패치에서 버그가 없고 개선점도 조정되었습니다.

35 changes: 35 additions & 0 deletions client/src/queries/post/useLikePostMutation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";
import axios from "@/libs/axios";
import { PostInterface } from "@/types/post/PostInterface";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getPostListInfiniteQueryKey } from "./useGetPostListInfiniteQuery";
/**
* 좋아요를 수행하고, 게시글을 invalidation 하는 쿼리
* @returns
*/
const useLikePostMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: PostInterface["postNo"]) => useLikePostMutationFn(id),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: getPostListInfiniteQueryKey.all,
}),
});
};

/**
* PostNo 를 기반으로 좋아요를 수행하는 함수
* @param id PostNo (게시글 PK)
* @returns
*/
export const useLikePostMutationFn = async (id: PostInterface["postNo"]) => {
const token = localStorage.getItem("accessToken");
// FIXME 리터럴제거
const { data } = await axios.post(`/posts/like/${id}`, null, {
headers: { Authorization: token },
});
return data;
};

export default useLikePostMutation;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위의 코드 패치는 좋아요 기능에 대한 코드로 보입니다. 몇 가지 개선 사항과 버그 리스크를 고려해 보겠습니다.

  1. Axios Import: 라이브러리 import 구문이 import axios from "@/libs/axios";로 되어 있습니다. 실제로 제공된 위치가 맞는지 확인하십시오. 명시적인 경로를 사용하는 대신, import axios from 'axios';를 사용하는 것이 더 안전합니다.

  2. useLikePostMutationFn 함수: 현재 이 함수는 접근 토큰을 로컬 스토리지에서 가져오고 사용하며, 예상되는 데이터를 반환합니다. 다음과 같은 개선 사항을 고려할 수 있습니다:

    • 토큰을 가져오는 방식에 대해 보다 안전한 방법을 사용하십시오 (예: 인증된 사용자 세션 또는 JWT).
    • 에러 처리를 추가하여 요청이 실패했을 때 적절한 조치를 취할 수 있도록 합니다.
  3. useLikePostMutation 함수: useMutation 후크를 사용하여 좋아요 작업을 초기화하고, 성공 시 쿼리를 재갱신합니다. 이 부분은 잘 구성되어 있는 것으로 보입니다.

  4. 리터럴 문자열: '/posts/like/${id}'입니다. 리터럴 문자열을 사용하는 것보다 변수를 활용하여 동적 경로를 만드는 것이 더 좋습니다. 예를 들어, '/posts/like/' + id 또는 템플릿 리터럴(/posts/like/${id})을 사용하십시오.

  5. 코드 일관성: 한 파일에 함수와 객체가 혼합되어 있습니다. 유지 관리 및 가독성을 위해 해당 함수들은 별도의 모듈로 분리하는 것이 좋습니다.

  6. 주석: 주석은 도움이 되지만, 올바른 맥락과 자세한 설명이 있는지 확인하십시오. 필요한 경우 주석을 추가하여 코드를 명확하게 설명할 수 있도록 합니다.

요약하면, 코드 패치는 대체로 잘 구성되어 있으며 작동할 것으로 보입니다. 개선 사항은 다음과 같습니다: Axios import 수정, useLikePostMutationFn 함수의 안전성 강화, 리터럴 문자열 대체, 코드 일관성 유지 및 주석의 가독성 향상입니다.

35 changes: 35 additions & 0 deletions client/src/queries/post/useUnLikePostMutation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";
import axios from "@/libs/axios";
import { PostInterface } from "@/types/post/PostInterface";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getPostListInfiniteQueryKey } from "./useGetPostListInfiniteQuery";
/**
* 좋아요를 취소하고, 게시글을 invalidation 하는 쿼리
* @returns
*/
const useLikePostMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: PostInterface["postNo"]) => useLikePostMutationFn(id),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: getPostListInfiniteQueryKey.all,
}),
});
};

/**
* PostNo 를 기반으로 좋아요를 취소하는 함수
* @param id PostNo (게시글 PK)
* @returns
*/
export const useLikePostMutationFn = async (id: PostInterface["postNo"]) => {
const token = localStorage.getItem("accessToken");
// FIXME 리터럴제거
const { data } = await axios.post(`/posts/like-cancel/${id}`, null, {
headers: { Authorization: token },
});
return data;
};

export default useLikePostMutation;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 리뷰를 해드리겠습니다.

  1. import 문을 이용하여 client 모듈과 axios 라이브러리 그리고 PostInterface와 관련된 타입들을 가져왔습니다.
  2. useMutationuseQueryClient 훅스를 사용했습니다.
  3. getPostListInfiniteQueryKey 함수를 이용하여 쿼리 키를 생성하고, invalidateQueries를 호출하여 해당 쿼리를 무효화합니다.
  4. useLikePostMutation 함수는 useMutation 훅을 반환합니다. 성공적으로 실행되면 onSuccess 핸들러에서 queryClient.invalidateQueries를 호출하여 해당 쿼리를 무효화합니다.
  5. useLikePostMutationFn은 게시글 번호 (id)를 기반으로 좋아요를 취소하는 비동기 함수입니다. accessToken을 로컬 스토리지에서 가져와 인증 헤더에 포함시킵니다. 주석에서 FIXME로 표기된 리터럴은 수정이 필요한 부분을 나타냅니다.
  6. useLikePostMutation 함수를 export 하여 다른 곳에서 사용할 수 있도록 합니다.
  7. default 키워드를 사용하여 모듈 전체를 useLikePostMutation으로 내보냅니다.

개선사항:

  • useLikePostMutationFn 함수를 export하여 다른 곳에서도 사용할 수 있도록 하였는데, 이 부분이 명확한지 확인해야 합니다.
  • 에러 핸들링을 추가하여 API 호출 중에 발생할 수 있는 예외 상황을 처리하는 것이 좋습니다.
  • 주석에 표시된 FIXME에서 리터럴 값을 제거하고 의미 있는 상수 또는 변수를 사용하는 것이 가독성면에서 좋습니다.

버그 및 위험 사항:

  • 코드상으로 큰 문제는 없어 보입니다. 그러나 API 호출을 하는 동안 발생할 수 있는 예외 상황에 대한 처리가 필요합니다.

8 changes: 7 additions & 1 deletion client/src/types/post/PostInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface PostInterface {
/**
* 이미지 Href 배열
*/
postAttachUrls: string[];
postAttachUrls: postAttachUrlsType[];
/**
* 사용자가 추가한 해시태그
*/
Expand Down Expand Up @@ -80,3 +80,9 @@ type QuoteInfoType = {
quoteNo: number;
quoteContent: string;
};

type postAttachUrlsType = {
attachNo:string;
attachUrl:string;
attachType:string
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주어진 코드 패치를 검토해보겠습니다.

주요 변경 사항:

  1. postAttachUrls의 타입이 string[]에서 postAttachUrlsType[]로 변경되었습니다.
  2. QuoteInfoType이 추가되었습니다.
  3. postAttachUrlsType이 추가되었으며, attachNo, attachUrl, attachType 속성을 포함하고 있습니다.

개선 제안:

  1. 코드 패치에는 명백한 버그 리스크가 보이지 않습니다.
  2. 커밋 메시지 및 주석 없이 코드만으로는 패치의 의도를 파악하기 어렵다는 점을 고려하여, 적절한 커밋 메시지와 설명 주석이 함께 제공되는 것이 좋습니다.
  3. 코드 스타일 및 구조에 관해서는 추가적인 정보가 없으므로 현재까지는 문제가 없어 보입니다.
  4. 다른 부분에서 발생할 수 있는 잠재적인 버그나 개선 가능한 점은 주어진 코드 패치의 외부에 있을 수도 있으므로 해당 부분을 확인할 필요가 있습니다.

Loading