diff --git a/src/apis/post/apis.ts b/src/apis/post/apis.ts index 51fa82e8..c5f1e127 100644 --- a/src/apis/post/apis.ts +++ b/src/apis/post/apis.ts @@ -4,7 +4,13 @@ import type { CreatePostReq, CreatePostRes, GetPostsReq, - GetPostsRes + GetPostsRes, + UpdateTradeStatusReq, + UpdateTradeStatusRes, + DeletePostReq, + DeletePostRes, + UpdatePostReq, + UpdatePostRes } from './types' import { http } from '@utils/http' @@ -14,8 +20,26 @@ export const getPost = (id: number) => export const createPost = (param: CreatePostReq) => http.post('/posts', param) +export const updatePost = ({ postId, ...params }: UpdatePostReq) => + http.put, UpdatePostRes>( + `/posts/${postId}`, + params + ) + export const getCategories = () => http.get('/categories') export const getPosts = (params: GetPostsReq) => http.get('/posts', params) + +export const updateTradeStatus = ({ + postId, + ...params +}: UpdateTradeStatusReq) => + http.put, UpdateTradeStatusRes>( + `/posts/trade-status/${postId}`, + params + ) + +export const deletePost = (postId: DeletePostReq) => + http.delete(`/posts/${postId}`) diff --git a/src/apis/post/queries.ts b/src/apis/post/queries.ts index adee3c70..8c253165 100644 --- a/src/apis/post/queries.ts +++ b/src/apis/post/queries.ts @@ -1,23 +1,69 @@ import { useMutation, useQuery, useInfiniteQuery } from '@tanstack/react-query' -import { getPost, getCategories, createPost, getPosts } from './apis' -import type { CreatePostReq, GetPostsReq, GetPostsRes } from './types' +import { + getPost, + getCategories, + createPost, + getPosts, + updateTradeStatus, + deletePost, + updatePost +} from './apis' +import type { + CreatePostReq, + DeletePostReq, + GetPostsReq, + GetPostsRes, + UpdatePostReq, + UpdateTradeStatusReq +} from './types' export const useCreatePostMutation = () => useMutation({ mutationFn: (param: CreatePostReq) => createPost(param) }) +export const useUpdatePostMutation = () => + useMutation({ + mutationFn: (params: UpdatePostReq) => updatePost(params) + }) + export const useGetPostQuery = (id: number) => useQuery({ queryKey: ['getPost', id], queryFn: () => getPost(id), + enabled: typeof id === 'number', select: data => ({ ...data, - postImages: - data.imageUrls.map((url, idx) => ({ - id: idx, + postForm: { + category: data.category.code, + tradeType: data.tradeType.code, + productCondition: data.productCondition.code, + price: String(data.price), + imageInfos: [ + { + id: '0', + isRepresent: true, + url: data.thumbnailImageUrl || '' + }, + ...(data.imageUrls.map((url, idx) => ({ + id: String(idx + 1), + url + })) || []) + ], + title: data.title, + description: data.description, + location: data.location + }, + postImages: [ + { + id: 0, + src: data.thumbnailImageUrl || '' + }, + ...(data.imageUrls.map((url, idx) => ({ + id: idx + 1, src: url - })) || [] + })) || []) + ] }) }) @@ -43,3 +89,13 @@ export const useGetInfinitePostsQuery = (params: GetPostsReq) => ? lastPage.posts[lastPage.posts.length - 1].id : undefined }) + +export const useUpdateTradeStatusMutation = () => + useMutation({ + mutationFn: (params: UpdateTradeStatusReq) => updateTradeStatus(params) + }) + +export const useDeletePostMutation = (postId: DeletePostReq) => + useMutation({ + mutationFn: () => deletePost(postId) + }) diff --git a/src/apis/post/types.ts b/src/apis/post/types.ts index 06ba534b..f5e6a1de 100644 --- a/src/apis/post/types.ts +++ b/src/apis/post/types.ts @@ -5,19 +5,20 @@ import type { PostSummaries, ProductConditionCodes, SortOptionsShape, - TradeStatusType, + TradeStatusCodes, TradeTypeCodes } from '@types' export type GetPostRes = PostDetail export type UpdatePostReq = { + postId: number title: string category: string price: number location: string productCondition: ProductConditionCodes - tradeStatus: TradeStatusType + tradeStatus: TradeStatusCodes tradeType: TradeTypeCodes thumbnailImageUrl: string imageUrls: string[] @@ -25,9 +26,7 @@ export type UpdatePostReq = { } export type UpdatePostRes = PostDetail -export type DeletePostReq = { - postId: number -} +export type DeletePostReq = number export type DeletePostRes = { // TODO: 정확한 타입 BE 확인 필요 } @@ -35,7 +34,7 @@ export type DeletePostRes = { // TODO: 정확한 타입 BE 확인 필요 export type UpdateTradeStatusReq = { postId: number - request: TradeStatusType + tradeStatus: TradeStatusCodes } export type UpdateTradeStatusRes = number diff --git a/src/components/common/Dialog/index.tsx b/src/components/common/Dialog/index.tsx index 2199312a..25ad6857 100644 --- a/src/components/common/Dialog/index.tsx +++ b/src/components/common/Dialog/index.tsx @@ -3,14 +3,12 @@ import type { DialogProps } from './types' const Dialog = ({ onClose, dialogPositionStyle, children }: DialogProps) => { return ( - <> - - - - {children} - - - + + + + {children} + + ) } diff --git a/src/components/post/UserProfile/index.tsx b/src/components/post/UserProfile/index.tsx index 8be92600..25321329 100644 --- a/src/components/post/UserProfile/index.tsx +++ b/src/components/post/UserProfile/index.tsx @@ -27,14 +27,16 @@ const UserProfile = ({ {level} - - {location} - {isOfferProfile && ` · ${tradeType}`} - {isOfferProfile && ( - - {date} - + <> + + {location} + {` · ${tradeType}`} + + + {date} + + )} diff --git a/src/components/post/UserProfile/types.ts b/src/components/post/UserProfile/types.ts index 01c8f47f..483b9346 100644 --- a/src/components/post/UserProfile/types.ts +++ b/src/components/post/UserProfile/types.ts @@ -3,7 +3,7 @@ import type { TradeTypeNames } from '@types' export type UserProfileProps = { image?: string nickName: string - location: string + location?: string type: 'offer' | 'basic' level: number date?: string diff --git a/src/pages/post/[postId]/index.tsx b/src/pages/post/[postId]/index.tsx index 902b4a9d..d6232a31 100644 --- a/src/pages/post/[postId]/index.tsx +++ b/src/pages/post/[postId]/index.tsx @@ -7,15 +7,30 @@ import { Text, IconButton, SelectBox, + Button, ImageModal } from '@offer-ui/react' import type { GetServerSideProps } from 'next' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' import type { ReactElement } from 'react' -import { getTimeDiffText, toLocaleCurrency } from '@utils/format' -import { useGetPostQuery } from '@apis' -import { UserProfile, PriceOfferCard, PostFieldList } from '@components' +import { getTimeDiffText, toLocaleCurrency, toQueryString } from '@utils/format' +import { + useDeletePostMutation, + useGetPostQuery, + useUpdateTradeStatusMutation +} from '@apis' +import { + UserProfile, + PriceOfferCard, + PostFieldList, + Dialog, + CommonModal +} from '@components' import { TRADE_STATUS } from '@constants' import { useAuth, useModal } from '@hooks' +import type { TradeStatusCodes, TradeStatusType } from '@types' type Props = { postId: number } export const getServerSideProps: GetServerSideProps = async ({ @@ -28,81 +43,152 @@ export const getServerSideProps: GetServerSideProps = async ({ const PostDetailPage = ({ postId }: Props): ReactElement => { const getPostQuery = useGetPostQuery(postId) + const updateTradeStatusMutation = useUpdateTradeStatusMutation() + const deletePostMutation = useDeletePostMutation(postId) + + const [tradeStatus, setTradeStatus] = useState() + const router = useRouter() + + const tradeStatusDialog = useModal() + const deleteModal = useModal() const { user } = useAuth() const imageModal = useModal() const isSeller = user.id === getPostQuery.data?.seller.id const postImages = getPostQuery.data?.postImages || [] + const handleChangeTradeStatus = async (status: TradeStatusType) => { + const nextStatusCode = status.code + + setTradeStatus(nextStatusCode) + + await updateTradeStatusMutation.mutateAsync({ + postId, + tradeStatus: nextStatusCode + }) + } + + const handleClickDelete = async () => { + await deletePostMutation.mutateAsync() + + router.replace('/') + } + return ( - -
-
- -
- -
- - {isSeller ? ( - <> - { - // do something - }} - /> - - - ) : ( - - {getPostQuery.data?.tradeStatus.code} - - )} - - - {getPostQuery.data?.category.name || ''} - - - {getPostQuery.data?.title || ''} - - - {toLocaleCurrency(Number(getPostQuery.data?.price))} - - -
- -
- 상품 정보 - - - - {getPostQuery.data?.description} + <> + +
+
+
- - - -
- - + +
+ + {isSeller ? ( + <> + + + + {tradeStatusDialog.isOpen && ( + + + + 수정하기 + + + 삭제하기 + + + + )} + + + ) : ( + + {getPostQuery.data?.tradeStatus.name} + + )} + + + {getPostQuery.data?.category.name || ''} + + + {getPostQuery.data?.title || ''} + + + {toLocaleCurrency(Number(getPostQuery.data?.price))} + + +
+ +
+ 상품 정보 + + + + {getPostQuery.data?.description} +
+ + +
+
+ + +
- + + 삭제 + , + + ]} + description="삭제한 게시글은 복구할 수 없어요." + isOpen={deleteModal.isOpen} + title="게시글을 삭제하시겠어요?" + onClose={deleteModal.closeModal} + /> + ) } @@ -179,6 +265,30 @@ const Content = styled.div` `} ` +const MoreButtonWrapper = styled.div` + position: relative; +` + +const DialogButtonContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + + width: 100%; +` + +const DialogButton = styled.button` + width: 100%; + padding: 4px 0; + border: none; + + background-color: transparent; + + text-align: left; + + cursor: pointer; +` + const ProductConditionSelectBox = styled(SelectBox)` margin: 33px 0 16px; diff --git a/src/pages/post/index.tsx b/src/pages/post/index.tsx index 25ec3d06..a5f165a9 100644 --- a/src/pages/post/index.tsx +++ b/src/pages/post/index.tsx @@ -10,21 +10,22 @@ import { Button, Divider } from '@offer-ui/react' -import type { - ImageInfo, - UploaderOnChangeHandler, - SelectOnChangeHandler, - InputProps -} from '@offer-ui/react' +import type { ImageInfo, InputProps } from '@offer-ui/react' +import type { GetServerSideProps } from 'next' import { useRouter } from 'next/router' -import type { ReactElement, ChangeEventHandler } from 'react' -import { useState } from 'react' -import { useCreateUploadImagesMutation } from '@apis/image' -import type { CreatePostReq } from '@apis/post' -import { useGetCategoriesQuery, useCreatePostMutation } from '@apis/post' +import type { ReactElement } from 'react' +import { useEffect, useState } from 'react' import { localeCurrencyToNumber } from '@utils/format' +import type { CreatePostReq } from '@apis' +import { + useGetCategoriesQuery, + useCreatePostMutation, + useUpdatePostMutation, + useGetPostQuery, + useCreateUploadImagesMutation +} from '@apis' import { PostForm } from '@components' -import { TRADE_TYPES, PRODUCT_CONDITIONS } from '@constants' +import { TRADE_TYPES, PRODUCT_CONDITIONS, TRADE_STATUS } from '@constants' import { useResponsive } from '@hooks' type PostFormState = Partial< @@ -33,9 +34,8 @@ type PostFormState = Partial< price?: string imageInfos?: ImageInfo[] } -type HandleUpdatePostForm = ChangeEventHandler< - HTMLTextAreaElement | HTMLInputElement | HTMLFormElement -> +type PostFormStateKeys = KeyOf +type PostFormStateValues = ValueOf const postFormKeys: (keyof PostFormState)[] = [ 'title', @@ -53,11 +53,32 @@ const isCompleteForm = ( ): postForm is Required => postFormKeys.every(key => Boolean(postForm[key])) -const PostPage = (): ReactElement => { - const postMutation = useCreatePostMutation() - const uploadImagesQuery = useCreateUploadImagesMutation() - const categoriesQuery = useGetCategoriesQuery() +const getImageFormData = (files: File[]) => { + const imageFormData = new FormData() + + files.forEach(file => imageFormData.append('files', file)) + + return imageFormData +} + +type Props = { editPostId: number; type: string } +export const getServerSideProps: GetServerSideProps = async ({ + query +}) => ({ + props: { + editPostId: Number(query.postId), + type: (query.type as string) || '' + } +}) + +const PostPage = ({ type, editPostId }: Props): ReactElement => { + const createPostMutation = useCreatePostMutation() + const getPostQuery = useGetPostQuery(editPostId) + const createUploadImagesMutation = useCreateUploadImagesMutation() + const getCategoriesQuery = useGetCategoriesQuery() + const updatePostMutation = useUpdatePostMutation() const router = useRouter() + const [postForm, setPostForm] = useState({}) const InputSize = useResponsive({ @@ -65,25 +86,10 @@ const PostPage = (): ReactElement => { tablet: '100%' }) - const handleUpdateCategory: SelectOnChangeHandler = ({ code }) => { - setPostForm({ - ...postForm, - category: code - }) - } - - const handleUpdateImageInfos: UploaderOnChangeHandler = ({ - images: imageInfos - }) => { - setPostForm(prev => ({ - ...prev, - imageInfos - })) - } - - const handleUpdatePostForm: HandleUpdatePostForm = (e): void => { - const { name, value } = e.target - + const handleUpdatePostForm = ( + name: PostFormStateKeys, + value: PostFormStateValues + ): void => { setPostForm({ ...postForm, [name]: value @@ -96,25 +102,51 @@ const PostPage = (): ReactElement => { } const { imageInfos, price, ...post } = postForm - const imageFormData = new FormData() + const imageFiles = imageInfos + .filter(({ file }) => Boolean(file)) + .map(({ file }) => file) as File[] + let imageUrls = imageInfos.filter(({ file }) => !file).map(({ url }) => url) + let postId = editPostId + + if (imageFiles.length > 0) { + const imageFormData = getImageFormData(imageFiles) - imageInfos.forEach( - info => info.file && imageFormData.append('files', info.file) - ) + const { imageUrls: newImageUrls } = + await createUploadImagesMutation.mutateAsync(imageFormData) + + imageUrls = imageUrls.concat(newImageUrls) + } - const { imageUrls } = await uploadImagesQuery.mutateAsync(imageFormData) const [thumbnailImageUrl, ...images] = imageUrls || [] - const res = await postMutation.mutateAsync({ + const nextPost = { ...post, imageUrls: images, price: localeCurrencyToNumber(price), - thumbnailImageUrl - }) + thumbnailImageUrl: thumbnailImageUrl + } + + if (type === 'update') { + await updatePostMutation.mutateAsync({ + postId, + ...nextPost, + tradeStatus: getPostQuery.data?.tradeStatus.code || TRADE_STATUS[0].code + }) + } else { + const res = await createPostMutation.mutateAsync(nextPost) + + postId = res.id + } - router.push(`/post/${res.id}`) + router.replace(`/post/${postId}`) } + useEffect(() => { + if (getPostQuery.data) { + setPostForm(getPostQuery.data.postForm) + } + }, [getPostQuery.data]) + return ( @@ -127,7 +159,8 @@ const PostPage = (): ReactElement => { maxLength={40} name="title" placeholder="제목을 입력해주세요(40자 이내)" - onChange={handleUpdatePostForm} + value={postForm.title} + onChange={e => handleUpdatePostForm('title', e.target.value)} /> {postForm.title?.length}/40 @@ -136,7 +169,9 @@ const PostPage = (): ReactElement => {
+ handleUpdatePostForm('imageInfos', images) + } />
@@ -150,10 +185,11 @@ const PostPage = (): ReactElement => { handleUpdatePostForm('category', code)} /> @@ -161,16 +197,18 @@ const PostPage = (): ReactElement => { isPrice name="price" placeholder="시작가를 입력하세요" + value={postForm.price} width={InputSize} - onChange={handleUpdatePostForm} + onChange={e => handleUpdatePostForm('price', e.target.value)} /> handleUpdatePostForm('location', e.target.value)} /> @@ -178,7 +216,10 @@ const PostPage = (): ReactElement => { direction="horizontal" formName="productCondition" items={PRODUCT_CONDITIONS} - onChange={handleUpdatePostForm} + selectedValue={postForm.productCondition} + onChange={e => + handleUpdatePostForm('productCondition', e.target.value) + } /> @@ -186,7 +227,8 @@ const PostPage = (): ReactElement => { direction="horizontal" formName="tradeType" items={TRADE_TYPES} - onChange={handleUpdatePostForm} + selectedValue={postForm.tradeType} + onChange={e => handleUpdatePostForm('tradeType', e.target.value)} /> @@ -195,7 +237,11 @@ const PostPage = (): ReactElement => { 상품 설명 - + handleUpdatePostForm('description', e.target.value)} + />