diff --git a/src/apis/message/apis.ts b/src/apis/message/apis.ts index 392bf9dd..06e22de2 100644 --- a/src/apis/message/apis.ts +++ b/src/apis/message/apis.ts @@ -1,5 +1,32 @@ -import type { CreateMessageRoomReq, CreateMessageRoomRes } from './types' +import type { + GetMessageRoomsReq, + GetMessageRoomsRes, + GetMessageReq, + GetMessageRes, + CreateMessageReq, + CreateMessageRes, + CreateMessageRoomReq, + CreateMessageRoomRes +} from './types' import { http } from '@utils/http' +export const getMessageRooms = (params: GetMessageRoomsReq) => + http.get('/msgrooms', params) + +export const getMessage = (params: GetMessageReq) => + http.get( + `/msgrooms/${params.msgRoomId}/msgs`, + params + ) + +export const createMessage = ({ messageRoomId, ...params }: CreateMessageReq) => + http.post, CreateMessageRes>( + `/msgrooms/${messageRoomId}/msgs`, + params + ) + +export const deleteMessageRoom = (messageRoomId: number) => + http.delete(`/msgrooms/${messageRoomId}`) + export const createMessageRoom = (params: CreateMessageRoomReq) => http.post('/msgrooms', params) diff --git a/src/apis/message/queries.ts b/src/apis/message/queries.ts index 4eb3b2ec..e126d5e0 100644 --- a/src/apis/message/queries.ts +++ b/src/apis/message/queries.ts @@ -1,6 +1,40 @@ -import { useMutation } from '@tanstack/react-query' -import { createMessageRoom } from './apis' -import type { CreateMessageRoomReq } from './types' +import { useMutation, useQuery } from '@tanstack/react-query' +import { + getMessageRooms, + getMessage, + createMessage, + deleteMessageRoom, + createMessageRoom +} from './apis' +import type { + CreateMessageReq, + GetMessageReq, + GetMessageRoomsReq, + CreateMessageRoomReq +} from './types' + +export const useGetMessageRoomsQuery = (params: GetMessageRoomsReq) => + useQuery({ + queryKey: ['getMessageRooms', params], + queryFn: () => getMessageRooms(params) + }) + +export const useGetMessageQuery = (params: GetMessageReq) => + useQuery({ + queryKey: ['getMessage', params], + queryFn: () => getMessage(params), + enabled: typeof params.msgRoomId === 'number' + }) + +export const useCreateMessageMutation = () => + useMutation({ + mutationFn: (params: CreateMessageReq) => createMessage(params) + }) + +export const useDeleteMessageRoomMutation = (messageRoomId: number) => + useMutation({ + mutationFn: () => deleteMessageRoom(messageRoomId) + }) export const useCreateMessageRoomMutation = () => useMutation({ diff --git a/src/apis/message/types.ts b/src/apis/message/types.ts index 8b54e077..18d8611a 100644 --- a/src/apis/message/types.ts +++ b/src/apis/message/types.ts @@ -1,6 +1,12 @@ -import type { CommonCreation, MessageRoomInfo, MessageInfo } from '@types' +import type { + CommonCreation, + MessageRoomInfo, + MessageInfo, + MessageSortTypeCodes +} from '@types' export type GetMessageRoomsReq = { + sort: MessageSortTypeCodes page: number } export type GetMessageRoomsRes = MessageRoomInfo[] @@ -17,7 +23,7 @@ export type GetMessageReq = { export type GetMessageRes = MessageInfo[] export type CreateMessageReq = { - msgRoomId: number + messageRoomId: number content: string } export type CreateMessageRes = CommonCreation diff --git a/src/apis/post/apis.ts b/src/apis/post/apis.ts index 43406e45..bce945ca 100644 --- a/src/apis/post/apis.ts +++ b/src/apis/post/apis.ts @@ -6,7 +6,11 @@ import type { GetPostsReq, GetPostsRes, UpdateTradeStatusReq, - UpdateTradeStatusRes + UpdateTradeStatusRes, + DeletePostReq, + DeletePostRes, + UpdatePostReq, + UpdatePostRes } from './types' import { http } from '@utils/http' @@ -16,14 +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 updatePostTradeStatus = (params: UpdateTradeStatusReq) => - http.put( - `/posts/trade-status/${params.postId}`, +export const updatePostTradeStatus = ({ + 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 d4961d9d..bdf7f886 100644 --- a/src/apis/post/queries.ts +++ b/src/apis/post/queries.ts @@ -1,18 +1,20 @@ -import type { DefaultError } from '@tanstack/react-query' import { useMutation, useQuery, useInfiniteQuery } from '@tanstack/react-query' import { getPost, getCategories, createPost, getPosts, - updatePostTradeStatus + updatePostTradeStatus, + deletePost, + updatePost } from './apis' import type { CreatePostReq, + DeletePostReq, GetPostsReq, GetPostsRes, - UpdateTradeStatusReq, - UpdateTradeStatusRes + UpdatePostReq, + UpdateTradeStatusReq } from './types' export const useCreatePostMutation = () => @@ -20,10 +22,49 @@ export const useCreatePostMutation = () => 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) + queryFn: () => getPost(id), + enabled: typeof id === 'number', + select: data => ({ + ...data, + 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 + })) || []) + ] + }) }) export const useGetCategoriesQuery = () => @@ -50,7 +91,12 @@ export const useGetInfinitePostsQuery = (params: GetPostsReq) => : undefined }) -export const usePostTradeStatusMutation = () => - useMutation({ - mutationFn: params => updatePostTradeStatus(params) +export const useUpdateTradeStatusMutation = () => + useMutation({ + mutationFn: (params: UpdateTradeStatusReq) => updatePostTradeStatus(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 7b6c4a2e..77f353b6 100644 --- a/src/apis/post/types.ts +++ b/src/apis/post/types.ts @@ -6,19 +6,19 @@ import type { ProductConditionCodes, SortOptionsShape, TradeStatusCodes, - TradeStatusType, 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[] @@ -26,9 +26,7 @@ export type UpdatePostReq = { } export type UpdatePostRes = PostDetail -export type DeletePostReq = { - postId: number -} +export type DeletePostReq = number export type DeletePostRes = { // TODO: 정확한 타입 BE 확인 필요 } 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/common/Header/SideBar/index.tsx b/src/components/common/Header/SideBar/index.tsx index 11ec4478..49e3f681 100644 --- a/src/components/common/Header/SideBar/index.tsx +++ b/src/components/common/Header/SideBar/index.tsx @@ -1,5 +1,4 @@ import { Avatar, Divider, Icon, Badge } from '@offer-ui/react' -import Link from 'next/link' import { useRouter } from 'next/router' import type { ReactElement } from 'react' import React from 'react' @@ -21,7 +20,7 @@ const NAV_DATA: NavDataType = [ }, { content: '쪽지함', - url: '/message', + url: '/messagebox', iconType: 'message' } ] @@ -32,11 +31,19 @@ export const SideBar = ({ isOpen, onClose }: SideBarProps): ReactElement => { const handleClickLogin = () => { router.replace(OAUTH_URL.KAKAO) + + onClose() + } + + const handleClickButton = (url: string) => { + router.push(url) + onClose() } const handleClickLogout = () => { handleLogout() + onClose() } @@ -64,14 +71,14 @@ export const SideBar = ({ isOpen, onClose }: SideBarProps): ReactElement => { )} - {NAV_DATA.map((item, index) => { + {NAV_DATA.map(({ iconType, content, url }) => { return ( - - - - {item.content} - - + handleClickButton(url)}> + + {content} + ) })} diff --git a/src/components/common/Header/SideBar/styled.ts b/src/components/common/Header/SideBar/styled.ts index 660cce07..d6f22879 100644 --- a/src/components/common/Header/SideBar/styled.ts +++ b/src/components/common/Header/SideBar/styled.ts @@ -78,6 +78,8 @@ const SidebarMenu = styled.li` align-items: center; ${({ theme }): string => theme.fonts.body01B}; color: ${({ theme }): string => theme.colors.grayScale90}; + + cursor: pointer; ` const SidebarLogoutButton = styled.button` diff --git a/src/components/messagebox/Chatting/Chatting.stories.tsx b/src/components/messagebox/Chatting/Chatting.stories.tsx index 2aadcf1d..bea0fe67 100644 --- a/src/components/messagebox/Chatting/Chatting.stories.tsx +++ b/src/components/messagebox/Chatting/Chatting.stories.tsx @@ -12,61 +12,74 @@ export default meta const MESSAGES_MOCK = [ { - id: 10, content: 'offer 쪽지1', - receiverId: 1, - senderId: 2, - createdDate: '2021-11-11T00:12:43' + member: { + id: 1, + nickname: '주영', + imageUrl: '' + }, + sendTime: '2021-11-11T00:12:43' }, { - id: 8, content: 'offer 쪽지2', - receiverId: 1, - senderId: 2, - createdDate: '2021-12-15T00:13:49' + member: { + id: 2, + nickname: '수림', + imageUrl: '' + }, + sendTime: '2021-12-15T00:13:49' }, { - id: 6, content: ' offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3', - receiverId: 1, - senderId: 2, - createdDate: '2021-12-15T00:25:31' + member: { + id: 1, + nickname: '주영', + imageUrl: '' + }, + sendTime: '2021-12-15T00:25:31' }, { - id: 61, content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' + member: { + id: 2, + nickname: '수림', + imageUrl: '' + }, + sendTime: '2021-12-15T01:45:41' }, { - id: 62, content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' + member: { + id: 1, + nickname: '주영', + imageUrl: '' + }, + sendTime: '2021-12-15T01:45:41' }, { - id: 63, content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' + member: { + id: 1, + nickname: '주영', + imageUrl: '' + }, + sendTime: '2021-12-15T01:45:41' }, { - id: 64, content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' + member: { + id: 1, + nickname: '주영', + imageUrl: '' + }, + sendTime: '2021-12-15T01:45:41' } ] export const Default: StoryObj = { args: { userId: 1, - receiverImageUrl: 'https://picsum.photos/id/237/200/300', messages: MESSAGES_MOCK }, render: args => diff --git a/src/components/messagebox/Chatting/index.tsx b/src/components/messagebox/Chatting/index.tsx index d0d71969..58bf4142 100644 --- a/src/components/messagebox/Chatting/index.tsx +++ b/src/components/messagebox/Chatting/index.tsx @@ -4,53 +4,60 @@ import { Styled } from './styled' import type { ChattingProps } from './types' import { formatDate } from '@utils/format' -export const getDate = (createdDate = '') => createdDate.split('T')[0] - // TODO: 상품 정보 버블 추가 export const Chatting = ({ userId, messages, receiverImageUrl }: ChattingProps) => { - return ( - - {messages.map(({ id, content, senderId, createdDate }, idx, list) => { - const prevMessage = list.at(idx - 1) - const nextMessage = list.at(idx + 1) - const isSender = senderId === userId + const renderChattingList = () => + messages.map(({ content, member, sendTime }, idx, list) => { + const prevMessage = list.at(idx - 1) + const nextMessage = list.at(idx + 1) + const isSender = member.id === userId + + const currentDate = formatDate(sendTime, 'YYYY년 M월 D일 dddd') + const prevDate = formatDate( + prevMessage?.sendTime || '', + 'YYYY년 M월 D일 dddd' + ) + const nextDate = formatDate( + nextMessage?.sendTime || '', + 'YYYY년 M월 D일 dddd' + ) + const isPrevDateChanged = prevDate !== currentDate + const isNextDateChanged = nextDate !== currentDate + const currentTime = formatDate(sendTime, 'A H:mm') + const nextTime = formatDate(nextMessage?.sendTime || '', 'A H:mm') - const currentDate = getDate(createdDate) - const prevDate = getDate(prevMessage?.createdDate) - const nextDate = getDate(nextMessage?.createdDate) - const isPrevDateChanged = prevDate !== currentDate - const isNextDateChanged = nextDate !== currentDate + const commonProps = { + time: currentDate, + isSectionStart: + member.id !== prevMessage?.member.id || isPrevDateChanged, + isSectionLast: + member.id !== nextMessage?.member.id || + isNextDateChanged || + currentTime !== nextTime + } - const commonProps = { - time: formatDate(createdDate, 'A H:m'), - isSectionStart: - senderId !== prevMessage?.senderId || isPrevDateChanged, - isSectionLast: senderId !== nextMessage?.senderId || isNextDateChanged - } + return ( + // TODO: 메세지 키값 내려줄 수 있는지 확인하기 +
+ {isPrevDateChanged && ( + + {currentDate} + + )} + {isSender ? ( + {content} + ) : ( + + {content} + + )} +
+ ) + }) - return ( -
- {isPrevDateChanged && ( - - - {formatDate(createdDate, 'YYYY년 M월 D일 dddd')} - - - )} - {isSender ? ( - {content} - ) : ( - - {content} - - )} -
- ) - })} -
- ) + return {renderChattingList()} } diff --git a/src/components/messagebox/Chatting/types.ts b/src/components/messagebox/Chatting/types.ts index b365937d..918ee4b3 100644 --- a/src/components/messagebox/Chatting/types.ts +++ b/src/components/messagebox/Chatting/types.ts @@ -1,9 +1,11 @@ type Message = { - id: number + member: { + id: number + nickname: string + imageUrl: string + } content: string - receiverId: number - senderId: number - createdDate: string + sendTime: string } export type SendProps = { diff --git a/src/components/messagebox/ChattingRoom/ChattingRoom.stories.tsx b/src/components/messagebox/ChattingRoom/ChattingRoom.stories.tsx index f8f53e50..24acc76c 100644 --- a/src/components/messagebox/ChattingRoom/ChattingRoom.stories.tsx +++ b/src/components/messagebox/ChattingRoom/ChattingRoom.stories.tsx @@ -11,6 +11,6 @@ const meta: Meta = { export default meta export const Default: StoryObj = { - args: { id: 1 }, + args: { roomId: 1 }, render: args => } diff --git a/src/components/messagebox/ChattingRoom/index.tsx b/src/components/messagebox/ChattingRoom/index.tsx index 61d6affc..a1474991 100644 --- a/src/components/messagebox/ChattingRoom/index.tsx +++ b/src/components/messagebox/ChattingRoom/index.tsx @@ -1,114 +1,154 @@ -import { Image, IconButton, Input } from '@offer-ui/react' +import { Image, IconButton, Input, useMedia } from '@offer-ui/react' +import { useEffect, useRef, useState } from 'react' import { Styled } from './styled' import type { ChattingRoomProps } from './types' import { Chatting } from '../Chatting' +import type { ChattingProps } from '../Chatting/types' +import { Dialog } from '@components/common' import { toLocaleCurrency } from '@utils/format' +import { + useCreateMessageMutation, + useDeleteMessageRoomMutation, + useGetMessageQuery +} from '@apis' +import { useAuth, useModal } from '@hooks' -export const ChattingRoom = ({ id, onClose }: ChattingRoomProps) => { - // TODO: ChattingRoom Id를 통해서 컴포넌트 내부에서 패치하도록 +// TODO: messageRoom 정보조회 api 붙이고 제거 +const POST_MOCK = { + offerPrice: 10000, + post: { + title: '팔아요 !', + thumbnailImageUrl: '', + price: 12000 + } +} + +export const ChattingRoom = ({ roomId, onClose }: ChattingRoomProps) => { + const getMessageQuery = useGetMessageQuery({ + msgRoomId: roomId, + page: 0 + }) + const createMessageMutation = useCreateMessageMutation() + const deleteMessageRoomMutation = useDeleteMessageRoomMutation(roomId) + + const chattingBoxRef = useRef(null) + const inputRef = useRef(null) + + const [messages, setMessages] = useState([]) + const { desktop, tablet, mobile } = useMedia() + const { isOpen, openModal, closeModal } = useModal() + const { user } = useAuth() + + const senderInfo = { + id: user.id, + nickname: user.nickname, + imageUrl: user.profileImageUrl + } + + const scrollToBottom = () => { + if (!chattingBoxRef.current) { + return + } + + if (chattingBoxRef.current.scrollHeight > 0) { + chattingBoxRef.current.scrollTop = chattingBoxRef.current.scrollHeight + } + } const handleCloseRoom = () => { - onClose?.(id) + onClose?.(roomId) } + const handleDeleteRoom = async () => { + await deleteMessageRoomMutation.mutateAsync() + + handleCloseRoom() + } + + const handleSubmitMessage = async (content: string) => { + const res = await createMessageMutation.mutateAsync({ + messageRoomId: roomId, + content + }) + + if (inputRef.current) { + inputRef.current.value = '' + } + + setMessages(prev => [ + ...prev, + { + member: senderInfo, + content, + sendTime: res.createdAt + } + ]) + await getMessageQuery.refetch() + } + + useEffect(() => { + setMessages(getMessageQuery.data || []) + }, [getMessageQuery.data]) + + useEffect(() => { + scrollToBottom() + }, [messages, desktop, tablet, mobile]) + return ( - 황금 효정 + {user.nickname} - - + getMessageQuery.refetch()} + /> + + + {isOpen && ( + + + 쪽지함 나가기 + + + )} + - product + {`${POST_MOCK.post.title}-image`} - 마르니 플렛 로퍼(black) + {POST_MOCK.post.title} 시작가 - {toLocaleCurrency(15000)}원 + {toLocaleCurrency(POST_MOCK.post.price)}원 제안가 - {toLocaleCurrency(15000)}원 + + {toLocaleCurrency(POST_MOCK.offerPrice)}원 + - - + + - + ) } - -const MESSAGES_MOCK = [ - { - id: 10, - content: 'offer 쪽지1', - receiverId: 1, - senderId: 2, - createdDate: '2021-11-11T00:12:43' - }, - { - id: 8, - content: 'offer 쪽지2', - receiverId: 1, - senderId: 2, - createdDate: '2021-12-15T00:13:49' - }, - { - id: 6, - content: - ' offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3', - receiverId: 1, - senderId: 2, - createdDate: '2021-12-15T00:25:31' - }, - { - id: 61, - content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - }, - { - id: 62, - content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - }, - { - id: 63, - content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - }, - { - id: 64, - content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - }, - { - id: 612, - content: - 'offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3 offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - }, - { - id: 699, - content: 'offer 쪽지3', - receiverId: 2, - senderId: 1, - createdDate: '2021-12-15T01:45:41' - } -] diff --git a/src/components/messagebox/ChattingRoom/styled.ts b/src/components/messagebox/ChattingRoom/styled.ts index 60bf7e83..f1cf228c 100644 --- a/src/components/messagebox/ChattingRoom/styled.ts +++ b/src/components/messagebox/ChattingRoom/styled.ts @@ -34,6 +34,18 @@ const IconButtonContainer = styled.div` gap: 20px; ` +const MoreButtonWrapper = styled.div` + position: relative; +` + +const DeleteButton = styled.button` + border: none; + + background-color: transparent; + + cursor: pointer; +` + const ProductInfo = styled.div` display: flex; gap: 8px; @@ -106,6 +118,8 @@ export const Styled = { Header, Nickname, IconButtonContainer, + MoreButtonWrapper, + DeleteButton, ProductInfo, ProductTextContainer, ProductName, diff --git a/src/components/messagebox/ChattingRoom/types.ts b/src/components/messagebox/ChattingRoom/types.ts index 9080221b..a059f55c 100644 --- a/src/components/messagebox/ChattingRoom/types.ts +++ b/src/components/messagebox/ChattingRoom/types.ts @@ -1,4 +1,4 @@ export type ChattingRoomProps = { - id: number + roomId: number onClose?(id: number): void } diff --git a/src/components/messagebox/MessagePreview/MessagePreview.stories.tsx b/src/components/messagebox/MessagePreview/MessagePreview.stories.tsx index 82f84d9e..1ae96898 100644 --- a/src/components/messagebox/MessagePreview/MessagePreview.stories.tsx +++ b/src/components/messagebox/MessagePreview/MessagePreview.stories.tsx @@ -12,20 +12,21 @@ export default meta export const Default: StoryObj = { args: { - userInfo: { + partner: { id: 1, nickname: 'offerer', - profileImageUrl: null + imageUrl: '' }, - productInfo: { - price: 123346, - productImageUrl: null + post: { + id: 1, + price: 10000, + thumbnailImageUrl: '' }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } + offerPrice: 123, + lastContent: + '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', + lastSendTime: '2시간 전', + notReadCnt: 0 }, render: args => } diff --git a/src/components/messagebox/MessagePreview/index.tsx b/src/components/messagebox/MessagePreview/index.tsx index f291cb00..8bbda187 100644 --- a/src/components/messagebox/MessagePreview/index.tsx +++ b/src/components/messagebox/MessagePreview/index.tsx @@ -3,13 +3,16 @@ import type { ReactElement } from 'react' import { Styled } from './styled' import type { MessagePreviewProps } from './types' import { IMAGE } from '@constants' -import { toLocaleCurrency } from '@utils' +import { getTimeDiffText, toLocaleCurrency } from '@utils' export const MessagePreview = ({ id, - userInfo, - productInfo, - latestTalk, + partner, + post, + offerPrice, + lastContent, + lastSendTime, + notReadCnt, isSelected = false, onClick }: MessagePreviewProps): ReactElement => { @@ -23,31 +26,27 @@ export const MessagePreview = ({ role="button" onClick={handleClickPreview}> - + - {userInfo.nickname} - {latestTalk.createdDate} - {latestTalk.content} + {partner.nickname} + {getTimeDiffText(lastSendTime)} + {lastContent} - - 6 - - {`${toLocaleCurrency( - productInfo.price - )}원`} + {notReadCnt && ( + + {notReadCnt} + + )} + {`${toLocaleCurrency(offerPrice)}원`} product diff --git a/src/components/messagebox/MessagePreview/types.ts b/src/components/messagebox/MessagePreview/types.ts index 795a135c..8dde04a6 100644 --- a/src/components/messagebox/MessagePreview/types.ts +++ b/src/components/messagebox/MessagePreview/types.ts @@ -1,18 +1,6 @@ -export type MessagePreviewProps = { - id: number - userInfo: { - id: number - nickname: string - profileImageUrl: string | null - } - productInfo: { - price: number - productImageUrl: string | null - } - latestTalk: { - content: string - createdDate: string - } +import type { MessageRoomInfo } from '@types' + +export type MessagePreviewProps = MessageRoomInfo & { isSelected?: boolean onClick?(id: number): void } 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/constants/app.ts b/src/constants/app.ts index 9296a6e6..2ea307f1 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -34,6 +34,18 @@ export const TRADE_TYPES = [ } ] as const +export const MESSAGE_SORT_OPTIONS = [ + { code: 'ALL', name: '전체' }, + { + code: 'BUY', + name: '구매' + }, + { + code: 'SELL', + name: '판매' + } +] + export const TRADE_STATUS = [ { code: 'SELLING', diff --git a/src/pages/messagebox/index.tsx b/src/pages/messagebox/index.tsx index 75919c20..f7de2583 100644 --- a/src/pages/messagebox/index.tsx +++ b/src/pages/messagebox/index.tsx @@ -1,7 +1,11 @@ import { css } from '@emotion/react' import styled from '@emotion/styled' import { Modal, useMedia } from '@offer-ui/react' +import type { GetServerSideProps } from 'next' +import { useRouter } from 'next/router' import { useState, type ReactElement, useEffect } from 'react' +import { toQueryString } from '@utils/format' +import { useGetMessageRoomsQuery } from '@apis' import { MessagePreview, Tabs, @@ -9,35 +13,53 @@ import { ChattingRoom, MessageBoxPlaceholder } from '@components' -import { IMAGE } from '@constants' +import { IMAGE, MESSAGE_SORT_OPTIONS } from '@constants' import { useModal } from '@hooks' +import type { MessageSortTypeCodes } from '@types' -type TabType = 'all' | 'buy' | 'sell' +type RoomId = number | null +type Props = { + roomId: RoomId +} -const TABS = { - all: '전체', - buy: '구매', - sell: '판매' -} as const +export const getServerSideProps: GetServerSideProps = async ({ + query +}) => ({ + props: { + roomId: query.roomId ? Number(query.roomId) : null + } +}) -const TabKeys = Object.keys(TABS) as TabType[] -const TabEntries = Object.entries>(TABS) +const MessageBoxPage = ({ roomId: defaultRoomId }: Props): ReactElement => { + const [sortType, setSortType] = useState('ALL') + const getMessageRoomsQuery = useGetMessageRoomsQuery({ + page: 0, + sort: sortType + }) -const MessageBoxPage = (): ReactElement => { - const [tab, setTab] = useState('all') - const [roomId, setRoomId] = useState(null) + const [roomId, setRoomId] = useState(defaultRoomId) + const router = useRouter() const { isOpen, openModal, closeModal } = useModal() const { desktop, mobile, tablet } = useMedia() - const handleChangeTab = (currentIndex: number, nextIndex: number) => { - const nextTab = TabKeys[nextIndex] + const messageList = getMessageRoomsQuery.data || [] + const messagesCount = messageList.length + + const handleChangeSortType = (currentIndex: number, nextIndex: number) => { + const { code } = MESSAGE_SORT_OPTIONS[nextIndex] - setTab(nextTab) + setSortType(code) } const handleSelectRoom = (id: number) => { setRoomId(id) + router.push( + `/messagebox${toQueryString({ + roomId: String(id) + })}` + ) + if (!desktop) { openModal() } @@ -46,13 +68,23 @@ const MessageBoxPage = (): ReactElement => { const handleCloseRoom = () => { setRoomId(null) + getMessageRoomsQuery.refetch() + router.push(`/messagebox`) + if (!desktop) { closeModal() } } useEffect(() => { - setRoomId(null) + if (desktop) { + closeModal() + return + } + + if (roomId) { + openModal() + } }, [desktop, tablet, mobile]) return ( @@ -62,14 +94,16 @@ const MessageBoxPage = (): ReactElement => { - 내 쪽지함 {LIST_MOCK.length} + 내 쪽지함 {messagesCount}
- + - {TabEntries.map(([key, value]) => ( - - {value} + {MESSAGE_SORT_OPTIONS.map(({ code, name }) => ( + + + {name} + ))} @@ -77,14 +111,15 @@ const MessageBoxPage = (): ReactElement => {
- {LIST_MOCK.length > 0 ? ( - LIST_MOCK.map(({ id, ...messageInfo }) => ( + {messagesCount > 0 ? ( + messageList.map(({ id, post, ...resInfo }) => ( handleSelectRoom(id)} + {...resInfo} /> )) ) : ( @@ -101,7 +136,7 @@ const MessageBoxPage = (): ReactElement => {
{roomId ? ( - + ) : ( { {roomId && ( - + )} @@ -248,177 +283,4 @@ const DetailContainer = styled.div` `} ` -const LIST_MOCK = [ - { - id: 1, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 2, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 3, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 4, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 5, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 6, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 7, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 8, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 9, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - }, - { - id: 10, - userInfo: { - id: 1, - nickname: 'offerer', - profileImageUrl: null - }, - productInfo: { - price: 123346, - productImageUrl: null - }, - latestTalk: { - content: - '구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ? 구매 가능 할까요 ?', - createdDate: '2시간 전' - } - } -] - export default MessageBoxPage diff --git a/src/pages/post/[postId]/index.tsx b/src/pages/post/[postId]/index.tsx index d31a16e0..d6232a31 100644 --- a/src/pages/post/[postId]/index.tsx +++ b/src/pages/post/[postId]/index.tsx @@ -1,14 +1,36 @@ import { css } from '@emotion/react' import type { SerializedStyles } from '@emotion/react' import styled from '@emotion/styled' -import { Carousel, Divider, Text, IconButton, SelectBox } from '@offer-ui/react' +import { + Carousel, + Divider, + 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 } from '@hooks' +import { useAuth, useModal } from '@hooks' +import type { TradeStatusCodes, TradeStatusType } from '@types' type Props = { postId: number } export const getServerSideProps: GetServerSideProps = async ({ @@ -21,77 +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?.imageUrls.map((url, idx) => ({ - id: idx, - src: url - })) + 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} + /> + ) } @@ -145,6 +242,8 @@ const MainDivider = styled(Divider)` `} ` const Main = styled.div` + width: 100%; + ${({ theme }): SerializedStyles => css` ${theme.mediaQuery.tablet} { margin: 0; @@ -166,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; @@ -173,14 +296,11 @@ const ProductConditionSelectBox = styled(SelectBox)` ${theme.mediaQuery.tablet} { margin: 20px 0; } - ${theme.mediaQuery.mobile} { - margin: 20px 0; - } `} ` const ProductConditionBadge = styled.div` - margin-bottom: 20px; + margin: 33px 0 16px; padding: 4px 8px 3px; ${({ theme }) => css` @@ -191,6 +311,13 @@ const ProductConditionBadge = styled.div` color: ${theme.colors.white}; ${theme.fonts.body02B} + + ${theme.mediaQuery.tablet} { + margin: 20px 0; + } + ${theme.mediaQuery.mobile} { + margin: 20px 0; + } `} ` 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)} + /> diff --git a/src/pages/shop/panel/sale/index.tsx b/src/pages/shop/panel/sale/index.tsx index 5a6eac1f..6850036b 100644 --- a/src/pages/shop/panel/sale/index.tsx +++ b/src/pages/shop/panel/sale/index.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' import { Styled } from './styled' import type { GetPostsReq } from '@apis' import { - usePostTradeStatusMutation, + useUpdateTradeStatusMutation, useGetPostsQuery, useGetProfileQuery } from '@apis' @@ -28,7 +28,7 @@ export const ShopPageSalePanel = ({ const profile = useGetProfileQuery(memberId) const posts = useGetPostsQuery(searchOptions) - const postTradeStatus = usePostTradeStatusMutation() + const updateTradeStatus = useUpdateTradeStatusMutation() useEffect( function fetchPostsOnMount() { @@ -51,7 +51,7 @@ export const ShopPageSalePanel = ({ }) const handleChangeProductTradeStatus: SaleTabPostProps['onChangeTradeStatus'] = async (postId, tradeStatus) => { - await postTradeStatus.mutateAsync({ + await updateTradeStatus.mutateAsync({ postId, tradeStatus: tradeStatus.code }) diff --git a/src/types/scheme.ts b/src/types/scheme.ts index 17243814..9a69f02f 100644 --- a/src/types/scheme.ts +++ b/src/types/scheme.ts @@ -156,7 +156,7 @@ export type OfferSummary = { export type MessageRoomInfo = { id: number partner: PartnerBrief - post: PostBrief + post: Pick offerPrice: number lastContent: string notReadCnt: number diff --git a/src/types/service.ts b/src/types/service.ts index e506fb4b..31dcaaa8 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -5,6 +5,7 @@ import type { TRADE_TYPES, TRADE_STATUS, PRODUCT_CONDITIONS, + MESSAGE_SORT_OPTIONS, VALID_NICKNAME_MESSAGE, SCORE } from '@constants' @@ -21,6 +22,12 @@ export type SortType = ValueOf export type SortTypeCodes = SortType['code'] export type SortTypeNames = SortType['name'] +/** 정렬 타입 - 메세지 */ +export type MessageSortTypes = typeof MESSAGE_SORT_OPTIONS +export type MessageSortType = ValueOf +export type MessageSortTypeCodes = MessageSortType['code'] +export type MessageSortTypeNames = MessageSortType['name'] + /** 거래 방식 */ export type TradeTypes = typeof TRADE_TYPES export type TradeType = ValueOf diff --git a/src/utils/format/index.ts b/src/utils/format/index.ts index 9279eb2d..3c37b714 100644 --- a/src/utils/format/index.ts +++ b/src/utils/format/index.ts @@ -5,7 +5,7 @@ dayjs.locale('ko') const DATE_FORMAT = { 'YYYY년 M월 D일 dddd': 'YYYY년 M월 D일 dddd', - 'A H:m': 'A H:m' + 'A H:mm': 'A H:mm' } export const formatDate = (