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

새 글 작성 술 검색기능 추가 #35

Merged
merged 3 commits into from
Nov 13, 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
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);
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
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>
);
};
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
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
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
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;
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
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[];
jobkaeHenry marked this conversation as resolved.
Show resolved Hide resolved
Expand Down