From e807e1d4c2e0dbae0160c121ef29c3414d9ef7a8 Mon Sep 17 00:00:00 2001 From: Jungu Lee <100949102+jobkaeHenry@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:39:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=88=20=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EC=88=A0=20=EA=B2=80=EC=83=89=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor : 술네임태그 컴포넌트 variant 분기 * New : 술리스트 패칭 쿼리 구현 * New : 술 검색 후 첨부 기능 추가 --- .../app/(protectedRoute)/new-post/page.tsx | 56 +++---- client/src/assets/icons/XIcon.svg | 11 ++ .../components/newpost/SearchAlcoholInput.tsx | 141 ++++++++++++++++++ client/src/components/post/AlcoleNameTag.tsx | 22 ++- client/src/const/serverPath.ts | 5 + .../alcohol/useGetAlcoholListQuery.tsx | 32 ++++ client/src/types/alcohol/AlcoholInterface.ts | 2 +- 7 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 client/src/assets/icons/XIcon.svg create mode 100644 client/src/components/newpost/SearchAlcoholInput.tsx create mode 100644 client/src/queries/alcohol/useGetAlcoholListQuery.tsx diff --git a/client/src/app/(protectedRoute)/new-post/page.tsx b/client/src/app/(protectedRoute)/new-post/page.tsx index 3e76d8b..786dda9 100644 --- a/client/src/app/(protectedRoute)/new-post/page.tsx +++ b/client/src/app/(protectedRoute)/new-post/page.tsx @@ -15,10 +15,9 @@ import { } from "@mui/material"; 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, useEffect, useState } from "react"; +import { ChangeEvent, useCallback, useEffect, useState } from "react"; import HOME from "@/const/clientPath"; import CameraIcon from "@/assets/icons/CameraIcon.svg"; import PinIcon from "@/assets/icons/PinIcon.svg"; @@ -27,23 +26,33 @@ import useNewPostMutation from "@/queries/newPost/useNewPostMutation"; import useNewAttachMutation from "@/queries/attach/useNewAttachMutation"; import { useInvalidatePostList } from "@/queries/post/useGetPostListInfiniteQuery"; import { useDeletePostMutation } from "@/queries/post/useDeletePostMutation"; +import { + NewPostRequestInterface, + NewPostRequest_AlCohol, +} from "@/types/newPost/NewPostInterface"; +import SearchAlcoholInput from "@/components/newpost/SearchAlcoholInput"; export default function NewpostPage() { const { setLoading } = useGlobalLoadingStore(); const router = useRouter(); const invalidatePreviousPost = useInvalidatePostList(); - const [formValue, setFormValue] = useState({ + const [formValue, setFormValue] = useState({ postContent: "", postType: "BASIC", positionInfo: "", tagList: [] as string[], }); + const [alcoholInfo, setAlcoholInfo] = useState(); + useEffect(() => { + console.log(alcoholInfo); + }, [alcoholInfo]); const [userTypedTag, setUserTypedTag] = useState(""); const [file, setFile] = useState(); const [fileUrl, setFileUrl] = useState(); const [isSuccess, SetIsSuccess] = useState(false); + useEffect(() => { if (!file) { return; @@ -63,11 +72,14 @@ export default function NewpostPage() { const { mutateAsync: attachFileHandler } = useNewAttachMutation(); const { mutateAsync: deletePostHandler } = useDeletePostMutation(); - const submitHandler = async () => { + const submitHandler = useCallback(async () => { setLoading(true); let postNo; try { - const { postNo: res } = await newPostHandler(formValue); + const { postNo: res } = await newPostHandler({ + ...formValue, + ...alcoholInfo, + }); postNo = res; if (file) { try { @@ -88,7 +100,8 @@ export default function NewpostPage() { } finally { setLoading(false); } - }; + }, [formValue, alcoholInfo, router, router, file]); + return ( {/* 최상단 앱바 */} @@ -115,25 +128,15 @@ export default function NewpostPage() { {/* 검색창 */} - , - endAdornment: , - }} - onChange={changeHadler} - sx={{ px: 0 }} - /> - + + {/* 내용 */} - {formValue.postContent.length} /{" "} + {formValue.postContent!.length} /{" "} 200자 - - {formValue.tagList.map((tag) => { + + {formValue.tagList!.map((tag) => { return ( #{tag} @@ -166,11 +169,14 @@ export default function NewpostPage() { onSubmit={(e) => { e.preventDefault(); setFormValue((prev) => { - if (!userTypedTag || prev.tagList.includes(userTypedTag)) { + if (!userTypedTag || prev.tagList?.includes(userTypedTag)) { setUserTypedTag(""); return prev; } - return { ...prev, tagList: [...prev.tagList, userTypedTag] }; + return { + ...prev, + tagList: [...(prev?.tagList ?? []), userTypedTag], + }; }); setUserTypedTag(""); }} diff --git a/client/src/assets/icons/XIcon.svg b/client/src/assets/icons/XIcon.svg new file mode 100644 index 0000000..f4e6bc0 --- /dev/null +++ b/client/src/assets/icons/XIcon.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/client/src/components/newpost/SearchAlcoholInput.tsx b/client/src/components/newpost/SearchAlcoholInput.tsx new file mode 100644 index 0000000..ac2867a --- /dev/null +++ b/client/src/components/newpost/SearchAlcoholInput.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { + Box, + Chip, + CircularProgress, + List, + ListItemButton, + TextField, + Typography, +} from "@mui/material"; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react"; +import AlcholeSearchIcon from "@/assets/icons/AlcholeSearchIcon.svg"; +import InputSearchIcon from "@/assets/icons/InputSearchIcon.svg"; +import useGetAlcoholListQuery from "@/queries/alcohol/useGetAlcoholListQuery"; +import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; +import AlcoleNameTag from "./../post/AlcoleNameTag"; +import useDebounce from "@/hooks/useDebounce"; +import { NewPostRequest_AlCohol } from "@/types/newPost/NewPostInterface"; +import React from "react"; + +interface SearchAlcoholInputInterface { + setAlcoholInfo: Dispatch>; +} +const SearchAlcoholInput = ({ + setAlcoholInfo, +}: SearchAlcoholInputInterface) => { + const [searchKeyword, setSearchKeyword] = useState(""); + const debouncedValue = useDebounce(searchKeyword, 300); + + const [selectedAlcohol, setSelectedAlcohol] = + useState(); + + const { data, isLoading, isSuccess } = useGetAlcoholListQuery(debouncedValue); + const [isSearchingAlcohol, setIsSearchingAlCohol] = useState(false); + + const parsedDTO = useMemo(() => { + if (!selectedAlcohol) { + return; + } + const { alcoholNo, alcoholName, alcoholType, ...others } = selectedAlcohol; + return { + alcoholNo, + alcoholName, + alcoholType, + }; + }, [selectedAlcohol]); + + useEffect(() => { + setSearchKeyword(selectedAlcohol?.alcoholName ?? ""); + setAlcoholInfo(parsedDTO); + }, [selectedAlcohol]); + + return ( + <> + , + endAdornment: , + }} + onChange={({ target }) => setSearchKeyword(target.value)} + value={searchKeyword} + onFocus={() => setIsSearchingAlCohol(true)} + onBlur={() => setIsSearchingAlCohol(false)} + sx={{ px: 0 }} + autoComplete="off" + /> + {isSearchingAlcohol && ( + + + {isSuccess && + data?.list.map((alcoholData) => ( + e.preventDefault()} + onClick={() => { + setSelectedAlcohol(alcoholData); + setSearchKeyword(alcoholData.alcoholName); + setIsSearchingAlCohol(false); + }} + sx={ListItemButtonStyle} + > + + + + {alcoholData.alcoholName} + + + + + ))} + {isLoading && } + + + )} + {selectedAlcohol && ( + setSelectedAlcohol(undefined)} + removable + /> + )} + + ); +}; + +const WrapperStyle = { + width: "calc(100% - 32px)", + minHeight: "50px", + backgroundColor: "#F5F5F5", + border: "1px solid #E6E6E6", + borderRadius: 1.5, + position: "absolute", + top: "64px", + zIndex: 1, +}; +const ListStyle = { + display: "flex", + flexDirection: "column", + py: 1, + px: 2, + gap: 0.5, +}; +const ListItemButtonStyle = { + p: "1px", + borderRadius: 12, + justifyContent: "space-between", +}; +const FlexboxStyle = { + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: 1, +}; + +export default React.memo(SearchAlcoholInput); diff --git a/client/src/components/post/AlcoleNameTag.tsx b/client/src/components/post/AlcoleNameTag.tsx index 8394976..50c3d41 100644 --- a/client/src/components/post/AlcoleNameTag.tsx +++ b/client/src/components/post/AlcoleNameTag.tsx @@ -1,13 +1,21 @@ import { Box, Chip, IconButton, Typography } from "@mui/material"; import PostSeeMoreIcon from "@/assets/icons/PostSeeMoreIcon.svg"; import { PostInterface } from "@/types/post/PostInterface"; +import XIcon from "@/assets/icons/XIcon.svg"; type Props = { alcoholName: PostInterface["alcoholName"]; alcoholType: PostInterface["alcoholType"]; + removable?: boolean; + onClickRemove?: () => void; }; -const AlcoleNameTag = ({ alcoholName, alcoholType }: Props) => { +const AlcoleNameTag = ({ + alcoholName, + alcoholType, + removable = false, + onClickRemove, +}: Props) => { return ( { {alcoholName} - - - + {removable ? ( + onClickRemove && onClickRemove()}> + + + ) : ( + + + + )} ); }; diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts index fe9c809..15f5897 100644 --- a/client/src/const/serverPath.ts +++ b/client/src/const/serverPath.ts @@ -35,3 +35,8 @@ export const REMOVE_POST = (pk:number)=>`${POST_LIST}/${pk}` as const export type ATTACH_FILE_ResourceType = "POST" | "PROFILE" | "ALCOHOL"; export const ATTACH_FILE = (type: ATTACH_FILE_ResourceType, resourcePk: number) => `/attach/resources/${type}/${resourcePk}` as const; + +/** + * 알콜리스트를 받아오는 URL + */ +export const GET_ALCOHOL_LIST = '/alcohols' as const \ No newline at end of file diff --git a/client/src/queries/alcohol/useGetAlcoholListQuery.tsx b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx new file mode 100644 index 0000000..8a9f434 --- /dev/null +++ b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx @@ -0,0 +1,32 @@ +import { GET_ALCOHOL_LIST } from "@/const/serverPath"; +import axios from "@/libs/axios"; +import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; +import { useQuery } from "@tanstack/react-query"; + +const useGetAlcoholListQuery = (keyword: string) => { + return useQuery({ + queryKey: AlcohilListQueryKey.byKeyword(keyword), + queryFn: async () => await getAlcoholListByKeyword(keyword), + }); +}; + +export const getAlcoholListByKeyword = async (keyword: string) => { + const { data } = await axios.get<{ + list: AlcoholDetailInterface[]; + totalCount: number; + }>(GET_ALCOHOL_LIST, { + params: { + page: 0, + size: 10, + searchKeyword: keyword, + }, + }); + return data; +}; + +export const AlcohilListQueryKey = { + all: ["alcohol"] as const, + byKeyword: (keyword: string) => ["alcohol", keyword] as const, +}; + +export default useGetAlcoholListQuery; diff --git a/client/src/types/alcohol/AlcoholInterface.ts b/client/src/types/alcohol/AlcoholInterface.ts index 74b4ded..c2a5f2a 100644 --- a/client/src/types/alcohol/AlcoholInterface.ts +++ b/client/src/types/alcohol/AlcoholInterface.ts @@ -1,4 +1,4 @@ -export interface AlcoholDetailResponseInterface { +export interface AlcoholDetailInterface { alcoholNo: number; alcoholTypeNo: number; alcoholAttachUrls: AlcoholAttachUrlsInterface[];