Skip to content

Commit

Permalink
새 글 작성 술 검색기능 추가 (#35)
Browse files Browse the repository at this point in the history
* Refactor : 술네임태그 컴포넌트 variant 분기

* New : 술리스트 패칭 쿼리 구현

* New : 술 검색 후 첨부 기능 추가
  • Loading branch information
jobkaeHenry authored Nov 13, 2023
1 parent 6761162 commit e807e1d
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 30 deletions.
56 changes: 31 additions & 25 deletions client/src/app/(protectedRoute)/new-post/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<NewPostRequestInterface>({
postContent: "",
postType: "BASIC",
positionInfo: "",
tagList: [] as string[],
});

const [alcoholInfo, setAlcoholInfo] = useState<NewPostRequest_AlCohol>();
useEffect(() => {
console.log(alcoholInfo);
}, [alcoholInfo]);
const [userTypedTag, setUserTypedTag] = useState<string>("");
const [file, setFile] = useState<File>();
const [fileUrl, setFileUrl] = useState<string | ArrayBuffer | null>();
const [isSuccess, SetIsSuccess] = useState(false);

useEffect(() => {
if (!file) {
return;
Expand All @@ -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 {
Expand All @@ -88,7 +100,8 @@ export default function NewpostPage() {
} finally {
setLoading(false);
}
};
}, [formValue, alcoholInfo, router, router, file]);

return (
<Paper>
{/* 최상단 앱바 */}
Expand All @@ -115,25 +128,15 @@ export default function NewpostPage() {
<Paper
sx={{
display: "flex",
position: "relative",
flexDirection: "column",
gap: 2,
p: 2,
}}
>
{/* 검색창 */}
<TextField
placeholder="지금 어떤 술을 마시고 있나요?"
autoFocus
name="positionInfo"
size="small"
InputProps={{
startAdornment: <AlcholeSearchIcon />,
endAdornment: <InputSearchIcon />,
}}
onChange={changeHadler}
sx={{ px: 0 }}
/>

<SearchAlcoholInput setAlcoholInfo={setAlcoholInfo} />
{/* 내용 */}
<TextField
id="filled-multiline-flexible"
placeholder="입력해주세요"
Expand All @@ -147,13 +150,13 @@ export default function NewpostPage() {
/>

<Typography variant="label" sx={{ textAlign: "right" }}>
{formValue.postContent.length} /{" "}
{formValue.postContent!.length} /{" "}
<Typography variant="label" color="primary.main" component="span">
200자
</Typography>
</Typography>
<Box sx={{ display: "flex", gap: 1 }}>
{formValue.tagList.map((tag) => {
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
{formValue.tagList!.map((tag) => {
return (
<Typography variant="label" key={tag}>
#{tag}
Expand All @@ -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("");
}}
Expand Down
11 changes: 11 additions & 0 deletions client/src/assets/icons/XIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
141 changes: 141 additions & 0 deletions client/src/components/newpost/SearchAlcoholInput.tsx
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<NewPostRequest_AlCohol | undefined>>;
}
const SearchAlcoholInput = ({
setAlcoholInfo,
}: SearchAlcoholInputInterface) => {
const [searchKeyword, setSearchKeyword] = useState("");
const debouncedValue = useDebounce(searchKeyword, 300);

const [selectedAlcohol, setSelectedAlcohol] =
useState<AlcoholDetailInterface>();

const { data, isLoading, isSuccess } = useGetAlcoholListQuery(debouncedValue);
const [isSearchingAlcohol, setIsSearchingAlCohol] = useState(false);

const parsedDTO = useMemo<NewPostRequest_AlCohol | undefined>(() => {
if (!selectedAlcohol) {
return;
}
const { alcoholNo, alcoholName, alcoholType, ...others } = selectedAlcohol;
return {
alcoholNo,
alcoholName,
alcoholType,
};
}, [selectedAlcohol]);

useEffect(() => {
setSearchKeyword(selectedAlcohol?.alcoholName ?? "");
setAlcoholInfo(parsedDTO);
}, [selectedAlcohol]);

return (
<>
<TextField
placeholder="지금 어떤 술을 마시고 있나요?"
name="positionInfo"
size="small"
InputProps={{
startAdornment: <AlcholeSearchIcon />,
endAdornment: <InputSearchIcon />,
}}
onChange={({ target }) => setSearchKeyword(target.value)}
value={searchKeyword}
onFocus={() => setIsSearchingAlCohol(true)}
onBlur={() => setIsSearchingAlCohol(false)}
sx={{ px: 0 }}
autoComplete="off"
/>
{isSearchingAlcohol && (
<Box sx={WrapperStyle}>
<List sx={ListStyle}>
{isSuccess &&
data?.list.map((alcoholData) => (
<ListItemButton
key={alcoholData.alcoholNo}
disableRipple
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setSelectedAlcohol(alcoholData);
setSearchKeyword(alcoholData.alcoholName);
setIsSearchingAlCohol(false);
}}
sx={ListItemButtonStyle}
>
<Box sx={FlexboxStyle}>
<Chip label={alcoholData.alcoholType} variant="outlined" />
<Typography color="primary.main">
{alcoholData.alcoholName}
</Typography>
</Box>
<AlcholeSearchIcon />
</ListItemButton>
))}
{isLoading && <CircularProgress sx={{ margin: "0 auto" }} />}
</List>
</Box>
)}
{selectedAlcohol && (
<AlcoleNameTag
alcoholName={selectedAlcohol.alcoholName}
alcoholType={selectedAlcohol.alcoholType}
onClickRemove={() => 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);
22 changes: 18 additions & 4 deletions client/src/components/post/AlcoleNameTag.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box sx={WrapperStyle}>
<Box
Expand All @@ -28,9 +36,15 @@ const AlcoleNameTag = ({ alcoholName, alcoholType }: Props) => {
{alcoholName}
</Typography>
</Box>
<IconButton sx={{ p: 0 }}>
<PostSeeMoreIcon style={{ margin: "3px 0" }} />
</IconButton>
{removable ? (
<IconButton onClick={() => onClickRemove && onClickRemove()}>
<XIcon />
</IconButton>
) : (
<IconButton sx={{ p: 0 }}>
<PostSeeMoreIcon style={{ margin: "3px 0" }} />
</IconButton>
)}
</Box>
);
};
Expand Down
5 changes: 5 additions & 0 deletions client/src/const/serverPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions client/src/queries/alcohol/useGetAlcoholListQuery.tsx
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion client/src/types/alcohol/AlcoholInterface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface AlcoholDetailResponseInterface {
export interface AlcoholDetailInterface {
alcoholNo: number;
alcoholTypeNo: number;
alcoholAttachUrls: AlcoholAttachUrlsInterface[];
Expand Down

0 comments on commit e807e1d

Please sign in to comment.