diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 490d947c..cd2a0540 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -11,5 +11,6 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface ObjectConstructor { entries(o: { [s: T]: K } | ArrayLike): [T, K][] + keys(o: object): T[] } } diff --git a/src/apis/image/queries.ts b/src/apis/image/queries.ts index f9c4ecf8..0c523aad 100644 --- a/src/apis/image/queries.ts +++ b/src/apis/image/queries.ts @@ -1,8 +1,9 @@ +import type { DefaultError } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query' import { createUploadImages } from './apis' -import type { CreateUploadImagesReq } from './types' +import type { CreateUploadImagesReq, CreateUploadImagesRes } from './types' export const useCreateUploadImagesMutation = () => - useMutation({ - mutationFn: (files: CreateUploadImagesReq) => createUploadImages(files) + useMutation({ + mutationFn: files => createUploadImages(files) }) diff --git a/src/apis/image/types.ts b/src/apis/image/types.ts index edc6cada..0ee76c49 100644 --- a/src/apis/image/types.ts +++ b/src/apis/image/types.ts @@ -1,14 +1,9 @@ -import type { ImagesUpload, ImageUpload } from '@types' +import type { ImagesUpload } from '@types' export type CreateUploadImagesReq = FormData export type CreateUploadImagesRes = ImagesUpload -export type PostUploadImageReq = { - file: string -} -export type PostUploadImageRes = ImageUpload - export type GetImageReq = { path: string } diff --git a/src/apis/like/apis.ts b/src/apis/like/apis.ts index 3c05fd82..85f54553 100644 --- a/src/apis/like/apis.ts +++ b/src/apis/like/apis.ts @@ -1,5 +1,12 @@ -import type { UpdateLikeStatusReq } from './types' +import type { + GetLikedPostsReq, + GetLikedPostsRes, + UpdateLikeStatusReq +} from './types' import { http } from '@utils/http' export const updateLikeStatus = (postId: number) => http.put('/posts/likes', { postId }) + +export const getLikedPosts = (params: GetLikedPostsReq) => + http.get('/posts/likes', params) diff --git a/src/apis/like/index.ts b/src/apis/like/index.ts index d2186f10..7fece7cc 100644 --- a/src/apis/like/index.ts +++ b/src/apis/like/index.ts @@ -1,2 +1,3 @@ export * from './types' +export * from './apis' export * from './queries' diff --git a/src/apis/like/queries.ts b/src/apis/like/queries.ts index fe236e0e..58b08ee2 100644 --- a/src/apis/like/queries.ts +++ b/src/apis/like/queries.ts @@ -1,7 +1,15 @@ -import { useMutation } from '@tanstack/react-query' -import { updateLikeStatus } from './apis' +import type { DefaultError } from '@tanstack/react-query' +import { useQuery, useMutation } from '@tanstack/react-query' +import { getLikedPosts, updateLikeStatus } from './apis' +import type { GetLikedPostsReq, UpdateLikeStatusReq } from './types' + +export const useGetLikedPostsQuery = (searchOptions: GetLikedPostsReq) => + useQuery({ + queryKey: ['likedPosts', searchOptions], + queryFn: () => getLikedPosts(searchOptions) + }) export const useUpdateLikeStatusMutation = () => - useMutation({ - mutationFn: (postId: number) => updateLikeStatus(postId) + useMutation({ + mutationFn: postId => updateLikeStatus(postId) }) diff --git a/src/apis/like/types.ts b/src/apis/like/types.ts index 737aeb96..05ce8f37 100644 --- a/src/apis/like/types.ts +++ b/src/apis/like/types.ts @@ -2,8 +2,8 @@ import type { PostSummaries } from '@types' export type GetLikedPostsReq = { sort: string - lastId: number - limit: number + lastId?: number + limit?: number } export type GetLikedPostsRes = PostSummaries diff --git a/src/apis/member/apis.ts b/src/apis/member/apis.ts index b0493751..b6acd328 100644 --- a/src/apis/member/apis.ts +++ b/src/apis/member/apis.ts @@ -1,7 +1,11 @@ import type { GetMemberProfileRes, GetMemberProfileReq, - GetMyProfileRes + GetMyProfileRes, + CheckValidNicknameReq, + CheckValidNicknameRes, + UpdateMyProfileReq, + UpdateMyProfileRes } from './types' import { http } from '@utils/http' @@ -11,3 +15,18 @@ export const getMyProfile = () => export const getMemberProfile = async ( memberId: GetMemberProfileReq['memberId'] ) => http.get(`/member/${memberId}`) + +export const updateMyProfile = async ({ + memberId, + ...payload +}: UpdateMyProfileReq) => + http.put, UpdateMyProfileRes>( + `/member/${memberId}`, + payload + ) + +export const checkValidNickname = async (payload: CheckValidNicknameReq) => + http.post( + '/nickname-duplicate', + payload + ) diff --git a/src/apis/member/data.ts b/src/apis/member/data.ts index 23228f64..3a3ca95d 100644 --- a/src/apis/member/data.ts +++ b/src/apis/member/data.ts @@ -8,3 +8,13 @@ export const initialMyProfile = { reviewCount: 0, likeProductCount: 0 } + +export const initialMemberProfile = { + id: 0, + nickname: '', + profileImageUrl: '', + offerLevel: 0, + sellingProductCount: 0, + soldProductCount: 0, + reviewCount: 0 +} diff --git a/src/apis/member/queries.ts b/src/apis/member/queries.ts index 68621d57..bfb04132 100644 --- a/src/apis/member/queries.ts +++ b/src/apis/member/queries.ts @@ -1,6 +1,31 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query' -import { getMemberProfile, getMyProfile } from './apis' -import { initialMyProfile } from './data' +import type { DefaultError } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' +import { + checkValidNickname, + getMemberProfile, + getMyProfile, + updateMyProfile +} from './apis' +import { initialMemberProfile, initialMyProfile } from './data' +import type { + CheckValidNicknameReq, + CheckValidNicknameRes, + UpdateMyProfileReq, + UpdateMyProfileRes +} from './types' + +export const useGetProfileQuery = (memberId: null | number) => + useQuery({ + queryKey: ['profile', memberId], + queryFn: () => { + if (!memberId) { + return getMyProfile() + } + + return getMemberProfile(memberId) + }, + initialData: memberId ? initialMemberProfile : initialMyProfile + }) export const useGetMyProfileQuery = (accessToken?: string) => useQuery({ @@ -10,8 +35,16 @@ export const useGetMyProfileQuery = (accessToken?: string) => initialData: initialMyProfile }) -export const useGetMemberProfileQuery = (memberId = '') => - useSuspenseQuery({ - queryKey: ['memberProfile', memberId], - queryFn: () => getMemberProfile(Number(memberId)) +export const useUpdateMyProfileMutation = () => + useMutation({ + mutationFn: payload => updateMyProfile(payload) + }) + +export const useCheckValidNicknameMutation = () => + useMutation< + CheckValidNicknameRes, + DefaultError, + CheckValidNicknameReq['nickname'] + >({ + mutationFn: nickname => checkValidNickname({ nickname }) }) diff --git a/src/apis/member/types.ts b/src/apis/member/types.ts index b5508ac6..b6ea5771 100644 --- a/src/apis/member/types.ts +++ b/src/apis/member/types.ts @@ -7,18 +7,16 @@ export type GetMemberProfileReq = { } export type GetMemberProfileRes = MemberProfile -export type UpdateMemberProfileReq = { +export type UpdateMyProfileReq = { memberId: number nickname: string profileImageUrl: string } -// TODO: 정확한 타입 BE 확인 필요 -export type UpdateMemberProfileRes = number +export type UpdateMyProfileRes = number -export type CheckValidNickNameReq = { - nickName: string +export type CheckValidNicknameReq = { + nickname: string } -// TODO: 정확한 타입 BE 확인 필요 -export type CheckValidNickNameRes = { - [key in string]: boolean +export type CheckValidNicknameRes = { + duplicate: boolean } diff --git a/src/apis/offer/apis.ts b/src/apis/offer/apis.ts index 34ee66f9..cc440a5a 100644 --- a/src/apis/offer/apis.ts +++ b/src/apis/offer/apis.ts @@ -1,10 +1,15 @@ import type { + GetMyOffersReq, + GetMyOffersRes, GetPostOffersReq, CreateOfferReq, GetPostOffersRes } from './types' import { http } from '@utils/http' +export const getMyOffers = (params: GetMyOffersReq) => + http.get('/posts/offers', params) + export const getPostOffers = (params: GetPostOffersReq) => http.get( `/posts/${params.postId}/offers`, diff --git a/src/apis/offer/index.ts b/src/apis/offer/index.ts index d2186f10..7fece7cc 100644 --- a/src/apis/offer/index.ts +++ b/src/apis/offer/index.ts @@ -1,2 +1,3 @@ export * from './types' +export * from './apis' export * from './queries' diff --git a/src/apis/offer/queries.ts b/src/apis/offer/queries.ts index 60db6510..393771cc 100644 --- a/src/apis/offer/queries.ts +++ b/src/apis/offer/queries.ts @@ -1,6 +1,12 @@ import { useMutation, useQuery } from '@tanstack/react-query' -import { getPostOffers, createOffer } from './apis' -import type { CreateOfferReq, GetPostOffersReq } from './types' +import { getPostOffers, createOffer, getMyOffers } from './apis' +import type { CreateOfferReq, GetPostOffersReq, GetMyOffersReq } from './types' + +export const useGetMyOffersQuery = (searchOptions: GetMyOffersReq) => + useQuery({ + queryKey: ['myOffers', searchOptions], + queryFn: () => getMyOffers(searchOptions) + }) export const useGetPostOffersQuery = (params: GetPostOffersReq) => useQuery({ diff --git a/src/apis/offer/types.ts b/src/apis/offer/types.ts index 189cf928..556f85a0 100644 --- a/src/apis/offer/types.ts +++ b/src/apis/offer/types.ts @@ -21,7 +21,7 @@ export type CreateOfferRes = CommonCreation export type GetMyOffersReq = { sort: string - lastId: number - limit: number + lastId?: number + limit?: number } export type GetMyOffersRes = OfferSummaries diff --git a/src/apis/post/apis.ts b/src/apis/post/apis.ts index c5f1e127..bce945ca 100644 --- a/src/apis/post/apis.ts +++ b/src/apis/post/apis.ts @@ -32,7 +32,7 @@ export const getCategories = () => export const getPosts = (params: GetPostsReq) => http.get('/posts', params) -export const updateTradeStatus = ({ +export const updatePostTradeStatus = ({ postId, ...params }: UpdateTradeStatusReq) => diff --git a/src/apis/post/queries.ts b/src/apis/post/queries.ts index 8c253165..bdf7f886 100644 --- a/src/apis/post/queries.ts +++ b/src/apis/post/queries.ts @@ -4,7 +4,7 @@ import { getCategories, createPost, getPosts, - updateTradeStatus, + updatePostTradeStatus, deletePost, updatePost } from './apis' @@ -73,15 +73,16 @@ export const useGetCategoriesQuery = () => queryFn: getCategories }) -export const useGetPostsQuery = (params: GetPostsReq) => +export const useGetPostsQuery = (searchOptions: GetPostsReq) => useQuery({ - queryKey: ['getPosts'], - queryFn: () => getPosts(params) + queryKey: ['posts', searchOptions], + queryFn: () => getPosts(searchOptions), + enabled: typeof searchOptions.sellerId === 'number' }) export const useGetInfinitePostsQuery = (params: GetPostsReq) => useInfiniteQuery({ - queryKey: ['getPosts'], + queryKey: ['infinitePosts'], queryFn: () => getPosts(params), initialPageParam: null, getNextPageParam: lastPage => @@ -92,7 +93,7 @@ export const useGetInfinitePostsQuery = (params: GetPostsReq) => export const useUpdateTradeStatusMutation = () => useMutation({ - mutationFn: (params: UpdateTradeStatusReq) => updateTradeStatus(params) + mutationFn: (params: UpdateTradeStatusReq) => updatePostTradeStatus(params) }) export const useDeletePostMutation = (postId: DeletePostReq) => diff --git a/src/apis/post/types.ts b/src/apis/post/types.ts index f5e6a1de..77f353b6 100644 --- a/src/apis/post/types.ts +++ b/src/apis/post/types.ts @@ -31,7 +31,6 @@ export type DeletePostRes = { // TODO: 정확한 타입 BE 확인 필요 } -// TODO: 정확한 타입 BE 확인 필요 export type UpdateTradeStatusReq = { postId: number tradeStatus: TradeStatusCodes diff --git a/src/apis/review/apis.ts b/src/apis/review/apis.ts new file mode 100644 index 00000000..fb4e693e --- /dev/null +++ b/src/apis/review/apis.ts @@ -0,0 +1,20 @@ +import type { + GetReviewsReq, + GetReviewsRes, + CreateReviewReq, + CreateReviewRes, + GetReviewsCountsReq, + GetReviewsCountsRes +} from './types' +import { http } from '@utils/http' + +export const getReviews = (params: GetReviewsReq) => + http.get('/reviews', params) + +export const createReviews = (payload: CreateReviewReq) => + http.post('/reviews', payload) + +export const getReviewsCounts = (params: GetReviewsCountsReq) => + http.get( + `/reviews/counts?memberId=${params.memberId}` + ) diff --git a/src/apis/review/data.ts b/src/apis/review/data.ts new file mode 100644 index 00000000..49c01dca --- /dev/null +++ b/src/apis/review/data.ts @@ -0,0 +1,16 @@ +import type { TradeReviewActivityCodes } from '@types' + +export type SelectReviewCounts = { + [key in TradeReviewActivityCodes]: number +} + +export const initialReviewsCounts = { + all: 0, + seller: 0, + buyer: 0 +} + +export const initialReviews = { + hasNext: false, + reviews: [] +} diff --git a/src/apis/review/index.ts b/src/apis/review/index.ts index c9f6f047..7fece7cc 100644 --- a/src/apis/review/index.ts +++ b/src/apis/review/index.ts @@ -1 +1,3 @@ export * from './types' +export * from './apis' +export * from './queries' diff --git a/src/apis/review/queries.ts b/src/apis/review/queries.ts new file mode 100644 index 00000000..9a1d5c00 --- /dev/null +++ b/src/apis/review/queries.ts @@ -0,0 +1,33 @@ +import type { DefaultError } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' +import { createReviews, getReviews, getReviewsCounts } from './apis' +import type { SelectReviewCounts } from './data' +import { initialReviewsCounts, initialReviews } from './data' +import type { CreateReviewReq, CreateReviewRes, GetReviewsReq } from './types' +import type { ReviewCount } from '@types' + +export const useGetReviewsCountsQuery = (memberId: number) => + useQuery({ + queryKey: ['reviewsCounts', memberId], + queryFn: async () => getReviewsCounts({ memberId }), + select: ({ all, seller, buyer }) => ({ + ALL: all, + SELLER: seller, + BUYER: buyer + }), + enabled: Boolean(memberId), + initialData: initialReviewsCounts + }) + +export const useGetReviewsQuery = (searchOptions: GetReviewsReq) => + useQuery({ + queryKey: ['reviews', searchOptions], + queryFn: async () => getReviews({ ...searchOptions }), + enabled: Boolean(searchOptions.memberId), + initialData: initialReviews + }) + +export const useReviewsMutation = () => + useMutation({ + mutationFn: payload => createReviews(payload) + }) diff --git a/src/apis/review/types.ts b/src/apis/review/types.ts index d5b51ef4..26fa0bae 100644 --- a/src/apis/review/types.ts +++ b/src/apis/review/types.ts @@ -1,4 +1,4 @@ -import type { CommonCreation, ReviewInfo } from '@types' +import type { CommonCreation, ReviewCount, ReviewInfo } from '@types' export type GetReviewsReq = { memberId: number @@ -15,3 +15,8 @@ export type CreateReviewReq = { content: string } export type CreateReviewRes = CommonCreation + +export type GetReviewsCountsReq = { + memberId: number +} +export type GetReviewsCountsRes = ReviewCount diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index f283b85d..8072af74 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -59,7 +59,7 @@ const Header = (): ReactElement => { styleType="ghost" type="button" width="76px"> - + 나의 거래활동 diff --git a/src/components/shop/EditProfileModal/index.tsx b/src/components/shop/EditProfileModal/index.tsx index 8833713f..37723aa9 100644 --- a/src/components/shop/EditProfileModal/index.tsx +++ b/src/components/shop/EditProfileModal/index.tsx @@ -2,50 +2,114 @@ import { IconButton, Avatar, Modal, - Input, Button, Text, - Icon + Icon, + useImageUploader } from '@offer-ui/react' -import type { ReactElement } from 'react' +import type { ChangeEventHandler, ReactElement } from 'react' +import { useState } from 'react' import { Styled } from './styled' -import type { EditProfileModalProps } from './types' +import type { EditProfileForm, EditProfileModalProps } from './types' + +const NICK_NAME_MAX_LENGTH = 20 + +const initialProfileForm = { + image: { id: '', url: '', file: undefined }, + nickname: '' +} export const EditProfileModal = ({ isOpen, - onClose -}: EditProfileModalProps): ReactElement => ( - - - - - -

프로필 수정

-
- - - - - - - - - - - - 닉네임 - - - - 0/20 - - - 중복확인 - - - -
- -
-
-) + validate, + onValidateNickname, + onClose, + onConfirm, + onChangeImage +}: EditProfileModalProps): ReactElement => { + const [profileForm, setProfileForm] = + useState(initialProfileForm) + const { uploaderRef, openUploader, changeImage } = useImageUploader({ + onChange: async image => { + const uploadedImage = await onChangeImage(image) + setProfileForm({ ...profileForm, image: uploadedImage }) + } + }) + + const handleChangeNickname: ChangeEventHandler = e => { + setProfileForm({ ...profileForm, nickname: e.target.value }) + } + const handleClickDuplicateButton = () => { + onValidateNickname(profileForm.nickname.trim()) + } + const handleConfirm = () => { + onConfirm(profileForm) + } + + const handleClose = () => { + onClose?.() + setProfileForm(initialProfileForm) + } + + return ( + + + + + +

프로필 수정

+
+ + + + + + + + + + + + + 닉네임 + + + + {profileForm.nickname.length}/{NICK_NAME_MAX_LENGTH} + + {!!validate.message && ( + + {validate.message} + + )} + + 중복확인 + + + +
+ +
+
+ ) +} diff --git a/src/components/shop/EditProfileModal/styled.ts b/src/components/shop/EditProfileModal/styled.ts index e27cfedd..7fe44444 100644 --- a/src/components/shop/EditProfileModal/styled.ts +++ b/src/components/shop/EditProfileModal/styled.ts @@ -6,7 +6,7 @@ import { Button } from '@offer-ui/react' const Header = styled.div` text-align: center; - ${({ theme }): string => theme.fonts.headline01B} + ${({ theme }) => theme.fonts.headline01B} ` const CloseButtonWrapper = styled.div` @@ -32,6 +32,8 @@ const UploaderWrapper = styled.div` const AvatarWrapper = styled.div` position: relative; + + cursor: pointer; ` const CameraIconButton = styled.button` @@ -66,6 +68,10 @@ const DuplicateButton = styled(Button)` `}; ` +const UploaderInput = styled.input` + display: none; +` + export const Styled = { Header, CloseButtonWrapper, @@ -74,5 +80,6 @@ export const Styled = { AvatarWrapper, CameraIconButton, EditNickName, - DuplicateButton + DuplicateButton, + UploaderInput } diff --git a/src/components/shop/EditProfileModal/types.ts b/src/components/shop/EditProfileModal/types.ts index 6de0495e..99ec214b 100644 --- a/src/components/shop/EditProfileModal/types.ts +++ b/src/components/shop/EditProfileModal/types.ts @@ -1,3 +1,17 @@ import type { ModalProps } from '@offer-ui/react' -export type EditProfileModalProps = Pick +export type EditProfileForm = { + image: { id: string; file?: File; url: string } + nickname: string +} + +export type EditProfileValidate = { isSuccess: boolean; message: string } + +export type EditProfileModalProps = Pick & { + validate: EditProfileValidate + onValidateNickname(nickname: string): void + onConfirm(profile: EditProfileForm): void + onChangeImage( + image: EditProfileForm['image'] + ): Promise +} diff --git a/src/components/shop/Post/BuyTabPost/BuyTabPost.stories.tsx b/src/components/shop/Post/BuyTabPost/BuyTabPost.stories.tsx deleted file mode 100644 index 15fe2859..00000000 --- a/src/components/shop/Post/BuyTabPost/BuyTabPost.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Text } from '@offer-ui/react' -import type { Meta, StoryObj } from '@storybook/react' -import type { BuyTabPostProps } from './types' -import { BuyTabPost as BuyTabPostComponent } from './index' -import type { OfferSummary } from '@types' - -type BuyTabPost = typeof BuyTabPostComponent - -const meta: Meta = { - component: BuyTabPostComponent, - title: 'MyPage/Article/BuyTabPost' -} - -export default meta - -export const Primary: StoryObj = { - args: { - id: 0, - title: 'title', - price: 0, - location: '서울시', - thumbnailImageUrl: '', - liked: true, - tradeStatus: { code: 'SELLING', name: '판매중' }, - likeCount: 0, - createdAt: '' - }, - render: args => { - const post = args as OfferSummary - - return ( - <> - 가격제안 - - {/* TODO: 타입 이슈 디버깅 후 다시 확인 */} - {/* 관심상품 - */} - - ) - } -} diff --git a/src/components/shop/Post/BuyTabPost/index.tsx b/src/components/shop/Post/BuyTabPost/index.tsx deleted file mode 100644 index 349ed6fd..00000000 --- a/src/components/shop/Post/BuyTabPost/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ReactElement } from 'react' -import { LikeTabPanel } from './likeTabPanel' -import { OfferTabPanel } from './offerTabPanel' - -import type { BuyTabPostProps } from './types' - -const BuyTabPost = (props: BuyTabPostProps): ReactElement => { - const isOfferType = props.activityType === 'offer' - - return ( - <> - {isOfferType ? : } - - ) -} - -export { BuyTabPost, BuyTabPostProps } diff --git a/src/components/shop/Post/BuyTabPost/likeTabPanel/index.tsx b/src/components/shop/Post/BuyTabPost/likeTabPanel/index.tsx deleted file mode 100644 index 2ec7d77c..00000000 --- a/src/components/shop/Post/BuyTabPost/likeTabPanel/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { ReactElement } from 'react' -import type { LikeTabPanelProps } from './types' -import { Styled } from '../styled' -import { toLocaleCurrency } from '@utils' - -const LikeTabPanel = (props: LikeTabPanelProps): ReactElement => { - const { - className, - // sellerNickName, - id: postId, - thumbnailImageUrl, - title = '', - // startPrice, - tradeStatus, - createdAt, - likeCount - } = props - - // TODO: API Scheme 변경 필요 - const sellerNickName = '' - const startPrice = 0 - - return ( - - - - - - {sellerNickName} - - {title} - - - 시작가: {startPrice ? toLocaleCurrency(startPrice) : ''}원 - - - {tradeStatus.name} - - {createdAt} - - - - - 관심 {likeCount} - - - ) -} - -export { LikeTabPanel, LikeTabPanelProps } diff --git a/src/components/shop/Post/BuyTabPost/likeTabPanel/types.ts b/src/components/shop/Post/BuyTabPost/likeTabPanel/types.ts deleted file mode 100644 index d4e96ed3..00000000 --- a/src/components/shop/Post/BuyTabPost/likeTabPanel/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PostSummary } from '@types' - -export type LikeTabPanelProps = PostSummary & { className?: string } diff --git a/src/components/shop/Post/BuyTabPost/offerTabPanel/index.tsx b/src/components/shop/Post/BuyTabPost/offerTabPanel/index.tsx deleted file mode 100644 index 792b4eff..00000000 --- a/src/components/shop/Post/BuyTabPost/offerTabPanel/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { ReactElement } from 'react' -import type { OfferTabPanelProps } from './types' -import { Styled } from '../styled' -import { toLocaleCurrency } from '@utils' - -const OfferTabPanel = (props: OfferTabPanelProps): ReactElement => { - const { - className, - // sellerNickName, - postId, - thumbnailImageUrl, - // title = '', - offerPrice, - tradeStatus, - createdAt - // isReviewed - } = props - - // TODO: API Scheme 변경 필요 - const sellerNickName = '' - const title = '' - const isReviewed = false - - return ( - - - - - - {sellerNickName} - - {title} - - - 제안가: {offerPrice ? toLocaleCurrency(offerPrice) : ''}원 - - - {tradeStatus.name} - - {createdAt} - - - - - - {isReviewed ? '보낸 후기 보기' : '후기 보내기'} - - - - ) -} - -export { OfferTabPanel, OfferTabPanelProps } diff --git a/src/components/shop/Post/BuyTabPost/offerTabPanel/types.ts b/src/components/shop/Post/BuyTabPost/offerTabPanel/types.ts deleted file mode 100644 index a380d426..00000000 --- a/src/components/shop/Post/BuyTabPost/offerTabPanel/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { OfferSummary } from '@types' - -export type OfferTabPanelProps = OfferSummary & { className?: string } diff --git a/src/components/shop/Post/BuyTabPost/types.ts b/src/components/shop/Post/BuyTabPost/types.ts deleted file mode 100644 index 73bfff7e..00000000 --- a/src/components/shop/Post/BuyTabPost/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { OfferSummary, PostSummary, TradeBuyActivityCodes } from '@types' - -export type BuyTabPostProps = { className?: string } & ( - | LikeActivityProps - | OfferActivityProps -) - -export type LikeActivityProps = { - activityType: Extract -} & PostSummary -export type OfferActivityProps = { - activityType: Extract -} & OfferSummary diff --git a/src/components/shop/Post/index.tsx b/src/components/shop/Post/index.tsx deleted file mode 100644 index 374c156a..00000000 --- a/src/components/shop/Post/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './BuyTabPost' -export * from './SaleTabPost' -export * from './ReviewTabPost' diff --git a/src/components/shop/PostList/BuyTabPostList/BuyTabPostList.stories.tsx b/src/components/shop/PostList/BuyTabPostList/BuyTabPostList.stories.tsx deleted file mode 100644 index 87d37b86..00000000 --- a/src/components/shop/PostList/BuyTabPostList/BuyTabPostList.stories.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Text } from '@offer-ui/react' -import type { Meta, StoryObj } from '@storybook/react' -import { useState } from 'react' -import { BuyTabPostList as BuyTabPostListComponent } from './index' -import { TRADE_ACTIVITY_TYPES } from '@constants' -import type { TradeBuyActivityCodes } from '@types' - -type BuyTabArticleList = typeof BuyTabPostListComponent - -const meta: Meta = { - component: BuyTabPostListComponent, - title: 'MyPage/ArticleList/BuyTabArticleList' -} - -export default meta - -const PrimaryWithHooks = () => { - const [activityType, setActivityType] = - useState('offer') - - return ( - <> - - -
- - {TRADE_ACTIVITY_TYPES.buy[activityType]} - -
- - - ) -} -export const Primary: StoryObj = { - args: { activityType: 'like', posts: [] }, - render: () => -} diff --git a/src/components/shop/PostList/BuyTabPostList/index.tsx b/src/components/shop/PostList/BuyTabPostList/index.tsx deleted file mode 100644 index dcff7690..00000000 --- a/src/components/shop/PostList/BuyTabPostList/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Divider } from '@offer-ui/react' -import type { ReactElement } from 'react' -import { Fragment } from 'react' -import type { BuyTabPostListProps, PostType } from './types' -import { BuyTabPost } from '@components/shop/Post' -import type { OfferSummary } from '@types' - -const isOfferPost = (post: PostType): post is OfferSummary => 'postId' in post - -const BuyTabPostList = (props: BuyTabPostListProps): ReactElement => { - const getPostId = (post: PostType) => - isOfferPost(post) ? post.postId : post.id - - return ( -
    - {props.posts.map((post, index) => ( - - {isOfferPost(post) ? ( - - ) : ( - - )} - {index !== props.posts.length - 1 && } - - ))} -
- ) -} - -export { BuyTabPostList, BuyTabPostListProps } diff --git a/src/components/shop/PostList/BuyTabPostList/types.ts b/src/components/shop/PostList/BuyTabPostList/types.ts deleted file mode 100644 index 5a5d6701..00000000 --- a/src/components/shop/PostList/BuyTabPostList/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { OfferSummary, PostSummary, TradeBuyActivityCodes } from '@types' - -export type BuyTabPostListProps = { - className?: string -} & (LikePostListProps | OfferPostListProps) - -export type LikePostListProps = { - activityType: Extract - posts: PostSummary[] -} - -export type OfferPostListProps = { - activityType: Extract - posts: OfferSummary[] -} - -export type PostType = OfferSummary | PostSummary diff --git a/src/components/shop/PostList/index.tsx b/src/components/shop/PostList/index.tsx deleted file mode 100644 index bc27d7b7..00000000 --- a/src/components/shop/PostList/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './BuyTabPostList' -export * from './SaleTabPostList' -export * from './ReviewTabPostList' diff --git a/src/components/shop/ProfileBox/index.tsx b/src/components/shop/ProfileBox/index.tsx index a963e1ca..d6b63b6c 100644 --- a/src/components/shop/ProfileBox/index.tsx +++ b/src/components/shop/ProfileBox/index.tsx @@ -1,60 +1,71 @@ -import { Badge, Text, Icon } from '@offer-ui/react' +import { Badge, Text, Icon, useMedia } from '@offer-ui/react' import type { ReactElement } from 'react' import { Styled } from './styled' import type { ProfileBoxProps } from './types' const ProfileBox = ({ - nickname, - profileImageUrl, - likeProductCount, - offerLevel, - reviewCount, - sellingProductCount, - soldProductCount, - className + className, + isLogin, + onClickEditButton, + ...profile }: ProfileBoxProps): ReactElement => { + const { desktop } = useMedia() + return ( - - - + {isLogin && ( + + + + )} - + - {nickname} - Lv.{offerLevel} + {profile.nickname} + Lv.{profile.offerLevel} - - + + 판매중 - {sellingProductCount}개 + {profile.sellingProductCount}개 - + 거래완료 - {soldProductCount}개 + {profile.soldProductCount}개 - + - 거래후기 - - {reviewCount}개 - - - - - 관심상품 + 받은후기 - {likeProductCount}개 + {profile.reviewCount}개 + {isLogin && ( + + + + 관심상품 + + {profile.likeProductCount}개 + + )} diff --git a/src/components/shop/ProfileBox/styled.ts b/src/components/shop/ProfileBox/styled.ts index de98971b..fdb17309 100644 --- a/src/components/shop/ProfileBox/styled.ts +++ b/src/components/shop/ProfileBox/styled.ts @@ -1,9 +1,12 @@ +import type { Theme } from '@emotion/react' +import { css } from '@emotion/react' import styled from '@emotion/styled' import { Avatar as AvatarComponent } from '@offer-ui/react' const Container = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` position: relative; + width: 276px; height: 388px; padding: 26px; @@ -23,12 +26,15 @@ const Container = styled.div` `} ` const SettingsButton = styled.button` - ${({ theme }): string => ` + ${({ theme }) => css` float: right; - cursor: pointer; + + margin-top: -6px; border: none; + background: none; - margin-top: -6px; + + cursor: pointer; ${theme.mediaQuery.tablet} { margin-top: 0; @@ -37,7 +43,7 @@ const SettingsButton = styled.button` ` const ProfileWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` margin-top: 28px; ${theme.mediaQuery.tablet} { @@ -46,7 +52,7 @@ const ProfileWrapper = styled.div` `} ` const Avatar = styled(AvatarComponent)` - ${({ theme }): string => ` + ${({ theme }) => css` ${theme.avatar.medium}; ${theme.mediaQuery.tablet} { @@ -55,41 +61,46 @@ const Avatar = styled(AvatarComponent)` `} ` const UserWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; flex-direction: column; align-items: center; + margin-bottom: 24px; ${theme.mediaQuery.tablet} { flex-direction: row; align-items: flex-start; + margin-bottom: 16px; } -`} + `} ` const NickNameRow = styled.div` - ${({ theme }): string => ` - display: flex; - gap: 4px; - flex-direction: column; - margin-top: 14px; - align-items: center; + ${({ theme }) => css` + display: flex; + flex-direction: column; + gap: 4px; + align-items: center; - ${theme.mediaQuery.tablet} { - margin-top: 0; - margin-left: 12px; - align-items: flex-start; - } -`} + margin-top: 14px; + + ${theme.mediaQuery.tablet} { + align-items: flex-start; + + margin-top: 0; + margin-left: 12px; + } + `} ` const NickName = styled.span` - ${({ theme }): string => ` + ${({ theme }) => css` + overflow: hidden; ${theme.fonts.headline02B}; width: 180px; + text-align: center; text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; @@ -103,30 +114,82 @@ const NickName = styled.span` } `} ` -const UserProductWrapper = styled.div` - ${({ theme }): string => ` +const UserProductWrapper = styled.div<{ isLogin: boolean }>` + ${({ theme, isLogin }) => css` display: grid; - background-color: ${theme.colors.bgGray01}; gap: 16px; + padding: 16px 20px; + background-color: ${theme.colors.bgGray01}; + ${theme.mediaQuery.tablet} { - grid-template-columns: 1fr 1fr 1fr 1fr; + ${isLogin + ? 'grid-template-columns: 1fr 1fr 1fr 1fr;' + : 'grid-template-areas: ". sell sold review .";'} + gap: 93px; - padding: 24px 40px; + min-width: 684px; + padding: 24px 40px; } ${theme.mediaQuery.mobile} { + grid-template-areas: none; grid-template-columns: 1fr 1fr; gap: 44px; + min-width: 300px; padding: 20px 36px; } `} ` -const UserProductRow = styled.div` - ${({ theme }): string => ` + +const setStyleByToken = (theme: Theme, isLogin: boolean) => { + if (isLogin) { + return css` + ${theme.mediaQuery.mobile} { + min-width: 90px; + } + ` + } + + return css` + ${theme.mediaQuery.desktop} { + :nth-of-type(1), + :nth-of-type(2), + :nth-of-type(3) { + grid-area: auto; + } + } + + ${theme.mediaQuery.tablet} { + :nth-of-type(1) { + grid-area: sold; + } + + :nth-of-type(2) { + grid-area: sell; + } + + :nth-of-type(3) { + grid-area: review; + } + } + + ${theme.mediaQuery.mobile} { + min-width: 90px; + + :nth-of-type(1), + :nth-of-type(2), + :nth-of-type(3) { + grid-area: auto; + } + } + ` +} +const UserProductRow = styled.div<{ isLogin: boolean }>` + ${({ theme, isLogin }) => css` display: flex; align-items: center; justify-content: space-between; @@ -135,9 +198,7 @@ const UserProductRow = styled.div` min-width: 85px; } - ${theme.mediaQuery.mobile} { - min-width: 90px; - } + ${setStyleByToken(theme, isLogin)} `} ` const UserProductTitleWrapper = styled.p` diff --git a/src/components/shop/ProfileBox/types.ts b/src/components/shop/ProfileBox/types.ts index e132d18b..f9246da4 100644 --- a/src/components/shop/ProfileBox/types.ts +++ b/src/components/shop/ProfileBox/types.ts @@ -3,4 +3,8 @@ import type { MyProfile } from '@types' export type ProfileBoxProps = { className?: string likeProductCount?: number -} & Omit + isLogin: boolean + onClickEditButton(): void +} & Profile + +export type Profile = Omit diff --git a/src/components/shop/ReviewModal/Read/types.ts b/src/components/shop/ReviewModal/Read/types.ts deleted file mode 100644 index c57d6294..00000000 --- a/src/components/shop/ReviewModal/Read/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { CommonReviewModalProps, Score } from '../types' - -export type ReadReviewModalProps = CommonReviewModalProps & { - score: Score - content: string - onConfirm(): void -} diff --git a/src/components/shop/SelectBuyerModal/styled.ts b/src/components/shop/SelectBuyerModal/styled.ts index ab8242b2..ae3374f9 100644 --- a/src/components/shop/SelectBuyerModal/styled.ts +++ b/src/components/shop/SelectBuyerModal/styled.ts @@ -92,7 +92,7 @@ const BuyerInfo = styled.div` ` const Nickname = styled.span` - ${({ theme }): string => theme.fonts.body02B} + ${({ theme }) => theme.fonts.body02B} ` const OfferTime = styled.span` @@ -141,7 +141,7 @@ const Footer = styled.div` ` const SendReviewButton = styled(Button)` :disabled { - background-color: ${({ theme }): string => theme.colors.grayScale20}; + background-color: ${({ theme }) => theme.colors.grayScale20}; } ` diff --git a/src/components/shop/buy/LikeTabPost/index.tsx b/src/components/shop/buy/LikeTabPost/index.tsx new file mode 100644 index 00000000..afe25d46 --- /dev/null +++ b/src/components/shop/buy/LikeTabPost/index.tsx @@ -0,0 +1,56 @@ +import type { ReactElement } from 'react' +import { Styled } from './styled' +import type { LikeTabPostProps } from './types' +import { toLocaleCurrency, getTimeDiffText } from '@utils' + +const LikeTabPost = ({ + className, + seller, + id: postId, + thumbnailImageUrl, + title = '', + price, + tradeStatus, + createdAt, + likeCount, + onChangeLikeStatus +}: LikeTabPostProps): ReactElement => { + const handleChangeLikeStatus = () => { + onChangeLikeStatus(postId) + } + + return ( + + + + + + {seller.nickname} + + {title} + + 시작가: {toLocaleCurrency(price)}원 + + {tradeStatus.name} + + + {getTimeDiffText(createdAt)} + + + + + + 관심 {likeCount} + + + ) +} + +export { LikeTabPost, LikeTabPostProps } diff --git a/src/components/shop/Post/BuyTabPost/styled.ts b/src/components/shop/buy/LikeTabPost/styled.ts similarity index 85% rename from src/components/shop/Post/BuyTabPost/styled.ts rename to src/components/shop/buy/LikeTabPost/styled.ts index da0a8601..9273e8bb 100644 --- a/src/components/shop/Post/BuyTabPost/styled.ts +++ b/src/components/shop/buy/LikeTabPost/styled.ts @@ -1,8 +1,9 @@ +import { css } from '@emotion/react' import styled from '@emotion/styled' import { Image, Text, Button } from '@offer-ui/react' export const Container = styled.li` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; align-items: center; justify-content: space-between; @@ -15,17 +16,19 @@ export const Container = styled.li` `} ` export const ProductWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: grid; flex: 1; grid-template-columns: 90px 1fr; - align-items: center; gap: 16px; + align-items: center; + padding: 20px 0 20px 20px; ${theme.mediaQuery.tablet} { grid-template-columns: 68px 1fr; gap: 8px; + padding: 16px 24px; } @@ -36,7 +39,7 @@ export const ProductWrapper = styled.div` `} ` export const ProductImg = styled(Image)` - ${({ theme }): string => ` + ${({ theme }) => css` width: 90px; height: 90px; @@ -47,46 +50,51 @@ export const ProductImg = styled(Image)` `} ` export const SellerName = styled(Text)` - ${({ theme }): string => ` - text-align: center; - color: ${theme.colors.grayScale70}; + ${({ theme }) => css` + overflow: hidden; + max-width: 100px; + + color: ${theme.colors.grayScale70}; + text-align: center; text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; - ${theme.mediaQuery.tablet} { - ${theme.fonts.caption01M}; - } -`} + ${theme.mediaQuery.tablet} { + ${theme.fonts.caption01M}; + } + `} ` export const ProductMetaWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; align-items: center; justify-content: space-around; ${theme.mediaQuery.tablet} { - align-items: flex-start; flex-direction: column; + align-items: flex-start; } `} ` export const ProductName = styled(Text)` - ${({ theme }): string => ` - text-align: center; + ${({ theme }) => css` + overflow: hidden; + width: 150px; + + text-align: center; text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; ${theme.mediaQuery.tablet} { - text-align: left; max-width: 460px; margin-bottom: 6px; + + text-align: left; } ${theme.mediaQuery.mobile} { @@ -95,21 +103,21 @@ export const ProductName = styled(Text)` `} ` export const ProductInfoWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; + gap: 12px; align-items: center; justify-content: space-around; - gap: 12px; ${theme.mediaQuery.tablet} { - align-items: flex-start; flex-direction: column; gap: 0; + align-items: flex-start; } -`} + `} ` export const Price = styled.span` - ${({ theme }): string => ` + ${({ theme }) => css` ${theme.fonts.body02R}; ${theme.mediaQuery.tablet} { @@ -119,11 +127,13 @@ export const Price = styled.span` `} ` export const TradeStatusName = styled(Text)` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; flex-direction: column; align-items: center; + width: 100px; + color: ${theme.colors.grayScale50}; ${theme.mediaQuery.tablet} { @@ -132,8 +142,9 @@ export const TradeStatusName = styled(Text)` `} ` export const Date = styled(Text)` - ${({ theme }): string => ` + ${({ theme }) => css` display: inline-block; + color: ${theme.colors.grayScale50}; ${theme.mediaQuery.tablet} { @@ -144,38 +155,41 @@ export const Date = styled(Text)` `} ` export const ReviewButtonWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; flex-direction: column; - min-width: 120px; align-items: flex-end; + min-width: 120px; + ${theme.mediaQuery.tablet} { display: flex; align-items: center; } `} ` -export const ReviewButton = styled(Button)<{ isReviewed: boolean }>` - ${({ theme, isReviewed }): string => ` - color: ${isReviewed ? theme.colors.grayScale70 : theme.colors.brandPrimary}; +export const ReviewButton = styled(Button)<{ hasReview: boolean }>` + ${({ theme, hasReview }) => css` margin-right: 20px; + color: ${hasReview ? theme.colors.grayScale70 : theme.colors.brandPrimary}; + ${theme.mediaQuery.tablet} { width: 100%; + margin-right: 0; + padding: 20px 0; border: none; border-top: 1px solid ${theme.colors.grayScale10}; border-radius: 0; - padding: 20px 0; - margin-right: 0; } `} ` export const LikeButton = styled(Button)` - ${({ theme }): string => ` - color: ${theme.colors.grayScale90}; + ${({ theme }) => css` margin-right: 20px; + color: ${theme.colors.grayScale90}; + ${theme.mediaQuery.tablet} { display: none; } diff --git a/src/components/shop/buy/LikeTabPost/types.ts b/src/components/shop/buy/LikeTabPost/types.ts new file mode 100644 index 00000000..dd2b4cdb --- /dev/null +++ b/src/components/shop/buy/LikeTabPost/types.ts @@ -0,0 +1,6 @@ +import type { PostSummary } from '@types' + +export type LikeTabPostProps = PostSummary & { + className?: string + onChangeLikeStatus(postId: number): void +} diff --git a/src/components/shop/buy/LikeTabPostList/index.tsx b/src/components/shop/buy/LikeTabPostList/index.tsx new file mode 100644 index 00000000..d3ce3180 --- /dev/null +++ b/src/components/shop/buy/LikeTabPostList/index.tsx @@ -0,0 +1,22 @@ +import { Divider } from '@offer-ui/react' +import type { ReactElement } from 'react' +import { Fragment } from 'react' +import type { LikePostListProps } from './types' +import { LikeTabPost } from '../LikeTabPost' + +const LikeTabPostList = ({ + className, + posts, + onChangeProductLikeStatus +}: LikePostListProps): ReactElement => ( +
    + {posts.map((post, index) => ( + + + {index !== posts.length - 1 && } + + ))} +
+) + +export { LikeTabPostList, LikePostListProps } diff --git a/src/components/shop/buy/LikeTabPostList/types.ts b/src/components/shop/buy/LikeTabPostList/types.ts new file mode 100644 index 00000000..283e682d --- /dev/null +++ b/src/components/shop/buy/LikeTabPostList/types.ts @@ -0,0 +1,8 @@ +import type { LikeTabPostProps } from '../LikeTabPost' +import type { PostSummary } from '@types' + +export type LikePostListProps = { + className?: string + posts: PostSummary[] + onChangeProductLikeStatus: LikeTabPostProps['onChangeLikeStatus'] +} diff --git a/src/components/shop/buy/OfferTabPost/index.tsx b/src/components/shop/buy/OfferTabPost/index.tsx new file mode 100644 index 00000000..fb060d1f --- /dev/null +++ b/src/components/shop/buy/OfferTabPost/index.tsx @@ -0,0 +1,69 @@ +import type { ReactElement } from 'react' +import { Styled } from './styled' +import type { OfferTabPostProps } from './types' +import { toLocaleCurrency, getTimeDiffText } from '@utils' + +const OfferTabPost = ({ + className, + postId, + thumbnailImageUrl, + title, + seller, + offerPrice, + tradeStatus, + createdAt, + hasReview, + onClickReadReview, + onClickWriteReview, + reviewAvailable +}: OfferTabPostProps): ReactElement => { + const handleClickReviewButton = () => { + hasReview ? onClickReadReview() : onClickWriteReview() + } + const isShowReviewButton = hasReview || reviewAvailable + + return ( + + + + + + {seller.nickname} + + {title} + + + 제안가: {offerPrice ? toLocaleCurrency(offerPrice) : ''}원 + + + {tradeStatus.name} + + + {getTimeDiffText(createdAt)} + + + + + + {isShowReviewButton ? ( + + {hasReview ? '보낸 후기 보기' : '후기 보내기'} + + ) : ( + + - + + )} + + + ) +} + +export { OfferTabPost, OfferTabPostProps } diff --git a/src/components/shop/buy/OfferTabPost/styled.ts b/src/components/shop/buy/OfferTabPost/styled.ts new file mode 100644 index 00000000..b8dd5859 --- /dev/null +++ b/src/components/shop/buy/OfferTabPost/styled.ts @@ -0,0 +1,217 @@ +import { css } from '@emotion/react' +import styled from '@emotion/styled' +import { Image, Text, Button } from '@offer-ui/react' + +export const Container = styled.li` + ${({ theme }) => css` + display: flex; + align-items: center; + justify-content: space-between; + + ${theme.mediaQuery.tablet} { + flex-direction: column; + align-items: unset; + justify-content: unset; + } + `} +` +export const ProductWrapper = styled.div` + ${({ theme }) => css` + display: grid; + flex: 1; + grid-template-columns: 90px 1fr; + gap: 16px; + align-items: center; + + padding: 20px 0 20px 20px; + + ${theme.mediaQuery.tablet} { + grid-template-columns: 68px 1fr; + gap: 8px; + + padding: 16px 24px; + } + + ${theme.mediaQuery.mobile} { + min-width: 390px; + padding: 16px; + } + `} +` +export const ProductImg = styled(Image)` + ${({ theme }) => css` + width: 90px; + height: 90px; + + ${theme.mediaQuery.tablet} { + width: 68px; + height: 68px; + } + `} +` +export const SellerName = styled(Text)` + ${({ theme }) => css` + overflow: hidden; + + max-width: 100px; + + color: ${theme.colors.grayScale70}; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-word; + + ${theme.mediaQuery.tablet} { + ${theme.fonts.caption01M}; + } + `} +` + +export const ProductMetaWrapper = styled.div` + ${({ theme }) => css` + display: flex; + align-items: center; + justify-content: space-around; + + ${theme.mediaQuery.tablet} { + flex-direction: column; + align-items: flex-start; + } + `} +` +export const ProductName = styled(Text)` + ${({ theme }) => css` + overflow: hidden; + + width: 150px; + + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-word; + + ${theme.mediaQuery.tablet} { + max-width: 460px; + margin-bottom: 6px; + + text-align: left; + } + + ${theme.mediaQuery.mobile} { + max-width: 171px; + } + `} +` +export const ProductInfoWrapper = styled.div` + ${({ theme }) => css` + display: flex; + gap: 12px; + align-items: center; + justify-content: space-around; + + ${theme.mediaQuery.tablet} { + flex-direction: column; + gap: 0; + align-items: flex-start; + } + `} +` +export const Price = styled.span` + ${({ theme }) => css` + ${theme.fonts.body02R}; + + ${theme.mediaQuery.tablet} { + ${theme.fonts.caption01M}; + color: ${theme.colors.grayScale50}; + } + `} +` +export const TradeStatusName = styled(Text)` + ${({ theme }) => css` + display: flex; + flex-direction: column; + align-items: center; + + width: 100px; + + color: ${theme.colors.grayScale50}; + + ${theme.mediaQuery.tablet} { + display: none; + } + `} +` +export const Date = styled(Text)` + ${({ theme }) => css` + display: inline-block; + + color: ${theme.colors.grayScale50}; + + ${theme.mediaQuery.tablet} { + display: none; + ${theme.fonts.caption01M}; + color: ${theme.colors.grayScale30}; + } + `} +` +export const ReviewButtonWrapper = styled.div` + ${({ theme }) => css` + display: flex; + flex-direction: column; + align-items: flex-end; + + min-width: 120px; + + ${theme.mediaQuery.tablet} { + display: flex; + align-items: center; + } + `} +` +export const ReviewButton = styled(Button)<{ hasReview: boolean }>` + ${({ theme, hasReview }) => css` + margin-right: 20px; + + color: ${hasReview ? theme.colors.grayScale70 : theme.colors.brandPrimary}; + + ${theme.mediaQuery.tablet} { + width: 100%; + margin-right: 0; + padding: 20px 0; + border: none; + border-top: 1px solid ${theme.colors.grayScale10}; + border-radius: 0; + } + `} +` +export const ReviewBlankButton = styled(Text)` + align-self: center; +` +export const LikeButton = styled(Button)` + ${({ theme }) => css` + margin-right: 20px; + + color: ${theme.colors.grayScale90}; + + ${theme.mediaQuery.tablet} { + display: none; + } + `} +` + +export const Styled = { + Container, + ProductWrapper, + ProductImg, + SellerName, + ProductMetaWrapper, + ProductName, + ProductInfoWrapper, + Price, + TradeStatusName, + Date, + ReviewButtonWrapper, + ReviewButton, + ReviewBlankButton, + LikeButton +} diff --git a/src/components/shop/buy/OfferTabPost/types.ts b/src/components/shop/buy/OfferTabPost/types.ts new file mode 100644 index 00000000..a0733022 --- /dev/null +++ b/src/components/shop/buy/OfferTabPost/types.ts @@ -0,0 +1,7 @@ +import type { OfferSummary } from '@types' + +export type OfferTabPostProps = OfferSummary & { + className?: string + onClickReadReview(): void + onClickWriteReview(): void +} diff --git a/src/components/shop/buy/OfferTabPostList/index.tsx b/src/components/shop/buy/OfferTabPostList/index.tsx new file mode 100644 index 00000000..3c4809a6 --- /dev/null +++ b/src/components/shop/buy/OfferTabPostList/index.tsx @@ -0,0 +1,27 @@ +import { Divider } from '@offer-ui/react' +import type { ReactElement } from 'react' +import { Fragment } from 'react' +import type { OfferPostListProps } from './types' +import { OfferTabPost } from '../OfferTabPost' + +const OfferTabPostList = ({ + className, + posts, + onClickReadReview, + onClickWriteReview +}: OfferPostListProps): ReactElement => ( +
    + {posts.map((post, index) => ( + + onClickReadReview(post.review)} + onClickWriteReview={() => onClickWriteReview(post)} + /> + {index !== posts.length - 1 && } + + ))} +
+) + +export { OfferTabPostList, OfferPostListProps } diff --git a/src/components/shop/buy/OfferTabPostList/types.ts b/src/components/shop/buy/OfferTabPostList/types.ts new file mode 100644 index 00000000..d4334fe3 --- /dev/null +++ b/src/components/shop/buy/OfferTabPostList/types.ts @@ -0,0 +1,8 @@ +import type { OfferSummary, Review } from '@types' + +export type OfferPostListProps = { + className?: string + posts: OfferSummary[] + onClickWriteReview(offer: OfferSummary): void + onClickReadReview(review: Review): void +} diff --git a/src/components/shop/ReviewModal/Read/index.tsx b/src/components/shop/buy/ReviewModal/Read/index.tsx similarity index 58% rename from src/components/shop/ReviewModal/Read/index.tsx rename to src/components/shop/buy/ReviewModal/Read/index.tsx index 39d5959f..011572be 100644 --- a/src/components/shop/ReviewModal/Read/index.tsx +++ b/src/components/shop/buy/ReviewModal/Read/index.tsx @@ -1,24 +1,35 @@ import type { ReactElement } from 'react' import type { ReadReviewModalProps } from './types' -import { CommonTitleContainer, MOCK_SCORE } from '..' +import { CommonTitleContainer, SCORE_OPTIONS } from '..' import { Styled } from '../styled' +import { SCORE } from '@constants' +import type { ScoreNames } from '@types' export const Read = ({ isOpen = true, onClose, onConfirm, - nickname = '닉네임', - productName = '상품이름', - score = 'smile', + reviewTargetMember, + score = 0, + post, content = '리뷰' }: ReadReviewModalProps): ReactElement => { + const scoreNames = Object.keys(SCORE) + + if (!reviewTargetMember) { + return <> + } + return ( - + - -

{MOCK_SCORE.find(scoreItem => scoreItem.state === score)?.text}

+ +

{SCORE_OPTIONS[score]?.text}

{content} diff --git a/src/components/shop/buy/ReviewModal/Read/types.ts b/src/components/shop/buy/ReviewModal/Read/types.ts new file mode 100644 index 00000000..16cc7a2c --- /dev/null +++ b/src/components/shop/buy/ReviewModal/Read/types.ts @@ -0,0 +1,7 @@ +import type { CommonReviewModalProps } from '../types' +import type { Review } from '@types' + +export type ReadReviewModalProps = Partial & + Review & { + onConfirm(): void + } diff --git a/src/components/shop/ReviewModal/ReadReviewModal.stories.tsx b/src/components/shop/buy/ReviewModal/ReadReviewModal.stories.tsx similarity index 97% rename from src/components/shop/ReviewModal/ReadReviewModal.stories.tsx rename to src/components/shop/buy/ReviewModal/ReadReviewModal.stories.tsx index 254cab42..d90ecf96 100644 --- a/src/components/shop/ReviewModal/ReadReviewModal.stories.tsx +++ b/src/components/shop/buy/ReviewModal/ReadReviewModal.stories.tsx @@ -30,7 +30,7 @@ export const Default: StoryObj = { nickname: '닉네임', productName: '상품이름', content: '리뷰', - score: 'smile' + score: 2 }, render: args => } diff --git a/src/components/shop/ReviewModal/Write/index.tsx b/src/components/shop/buy/ReviewModal/Write/index.tsx similarity index 82% rename from src/components/shop/ReviewModal/Write/index.tsx rename to src/components/shop/buy/ReviewModal/Write/index.tsx index 8c9da4b9..848c8c16 100644 --- a/src/components/shop/ReviewModal/Write/index.tsx +++ b/src/components/shop/buy/ReviewModal/Write/index.tsx @@ -1,9 +1,10 @@ import type { ChangeEventHandler, ReactElement } from 'react' import { useState } from 'react' import type { WriteReviewModalProps, ReviewState } from './types' -import { CommonTitleContainer, MOCK_SCORE } from '..' +import { CommonTitleContainer, SCORE_OPTIONS } from '..' import { Styled } from '../styled' import type { ScoreState } from '../types' +import { isNumber } from '@utils' export const Write = ({ isOpen = true, @@ -32,11 +33,20 @@ export const Write = ({ onConfirm(reviewState) } + const handleClose = () => { + onClose?.() + + setReviewState({ + reviewScore: null, + reviewText: '' + }) + } + return ( - + - {MOCK_SCORE.map(scoreItem => { + {SCORE_OPTIONS.map(scoreItem => { return ( {'후기 보내기'} diff --git a/src/components/shop/ReviewModal/Write/types.ts b/src/components/shop/buy/ReviewModal/Write/types.ts similarity index 72% rename from src/components/shop/ReviewModal/Write/types.ts rename to src/components/shop/buy/ReviewModal/Write/types.ts index 8b28ca63..e78e3ec2 100644 --- a/src/components/shop/ReviewModal/Write/types.ts +++ b/src/components/shop/buy/ReviewModal/Write/types.ts @@ -1,6 +1,6 @@ import type { CommonReviewModalProps, ScoreState } from '../types' -export type WriteReviewModalProps = CommonReviewModalProps & { +export type WriteReviewModalProps = Partial & { onConfirm(state: ReviewState): void } diff --git a/src/components/shop/ReviewModal/WriteReviewModal.stories.tsx b/src/components/shop/buy/ReviewModal/WriteReviewModal.stories.tsx similarity index 100% rename from src/components/shop/ReviewModal/WriteReviewModal.stories.tsx rename to src/components/shop/buy/ReviewModal/WriteReviewModal.stories.tsx diff --git a/src/components/shop/ReviewModal/index.tsx b/src/components/shop/buy/ReviewModal/index.tsx similarity index 87% rename from src/components/shop/ReviewModal/index.tsx rename to src/components/shop/buy/ReviewModal/index.tsx index 6961d982..2712d597 100644 --- a/src/components/shop/ReviewModal/index.tsx +++ b/src/components/shop/buy/ReviewModal/index.tsx @@ -1,10 +1,10 @@ import type { ReactElement } from 'react' import { Read } from './Read' import { Styled } from './styled' -import type { CommonReviewModalProps, SCORE } from './types' +import type { CommonReviewModalProps, ScoreOptions } from './types' import { Write } from './Write' -export const MOCK_SCORE: SCORE = [ +export const SCORE_OPTIONS: ScoreOptions = [ { state: 'smile', text: '좋아요' diff --git a/src/components/shop/ReviewModal/styled.ts b/src/components/shop/buy/ReviewModal/styled.ts similarity index 54% rename from src/components/shop/ReviewModal/styled.ts rename to src/components/shop/buy/ReviewModal/styled.ts index 100de28b..54cfcee1 100644 --- a/src/components/shop/ReviewModal/styled.ts +++ b/src/components/shop/buy/ReviewModal/styled.ts @@ -5,11 +5,11 @@ import type { StyledReviewStateProps } from './types' const ReviewModal = styled(Modal)` width: 400px; - ${({ theme }): string => theme.mediaQuery.tablet} { + ${({ theme }) => theme.mediaQuery.tablet} { width: 320px; } - ${({ theme }): string => theme.mediaQuery.mobile} { + ${({ theme }) => theme.mediaQuery.mobile} { width: 320px; } ` @@ -28,40 +28,40 @@ const FirstSection = styled.div` ` const NickName = styled.div` - color: ${({ theme }): string => theme.colors.brandPrimary}; - ${({ theme }): string => theme.fonts.headline01B}; + color: ${({ theme }) => theme.colors.brandPrimary}; + ${({ theme }) => theme.fonts.headline01B}; - ${({ theme }): string => theme.mediaQuery.tablet} { - ${({ theme }): string => theme.fonts.headline02B}; + ${({ theme }) => theme.mediaQuery.tablet} { + ${({ theme }) => theme.fonts.headline02B}; } - ${({ theme }): string => theme.mediaQuery.mobile} { - ${({ theme }): string => theme.fonts.headline02B}; + ${({ theme }) => theme.mediaQuery.mobile} { + ${({ theme }) => theme.fonts.headline02B}; } ` const NormalText = styled.span` - ${({ theme }): string => theme.fonts.headline01B}; - ${({ theme }): string => theme.mediaQuery.tablet} { - ${({ theme }): string => theme.fonts.headline02B}; + ${({ theme }) => theme.fonts.headline01B}; + ${({ theme }) => theme.mediaQuery.tablet} { + ${({ theme }) => theme.fonts.headline02B}; } - ${({ theme }): string => theme.mediaQuery.mobile} { - ${({ theme }): string => theme.fonts.headline02B}; + ${({ theme }) => theme.mediaQuery.mobile} { + ${({ theme }) => theme.fonts.headline02B}; } ` const ProductText = styled.div` margin-top: 8px; - color: ${({ theme }): string => theme.colors.grayScale70}; - ${({ theme }): string => theme.fonts.body01R}; + color: ${({ theme }) => theme.colors.grayScale70}; + ${({ theme }) => theme.fonts.body01R}; - ${({ theme }): string => theme.mediaQuery.tablet} { - ${({ theme }): string => theme.fonts.body02R}; + ${({ theme }) => theme.mediaQuery.tablet} { + ${({ theme }) => theme.fonts.body02R}; } - ${({ theme }): string => theme.mediaQuery.mobile} { - ${({ theme }): string => theme.fonts.body02R}; + ${({ theme }) => theme.mediaQuery.mobile} { + ${({ theme }) => theme.fonts.body02R}; } ` @@ -75,13 +75,13 @@ const ReviewIconContainer = styled.div` cursor: pointer; - ${({ theme }): string => theme.mediaQuery.tablet} { + ${({ theme }) => theme.mediaQuery.tablet} { gap: 32px; margin: 24px 0; } - ${({ theme }): string => theme.mediaQuery.mobile} { + ${({ theme }) => theme.mediaQuery.mobile} { gap: 32px; margin: 24px 0; @@ -93,22 +93,26 @@ const ReviewState = styled.button` display: flex; flex-direction: column; gap: 4px; - place-items: center center; border: none; background: none; + cursor: pointer; + place-items: center center; + * { - color: ${({ isFill, theme }): string => + color: ${({ isFill, theme }) => isFill ? theme.colors.brandPrimary : theme.colors.grayScale30}; - ${({ theme }): string => theme.fonts.body01M}; + ${({ theme }) => theme.fonts.body01M}; } ` const ReviewIcon = styled(Icon)` width: 40px; height: 40px; + + cursor: pointer; ` const ReadModeReviewContent = styled.div` @@ -116,8 +120,8 @@ const ReadModeReviewContent = styled.div` height: 120px; padding: 10px 12px; - background: ${({ theme }): string => theme.colors.bgGray02}; - ${({ theme }): string => theme.fonts.body02M}; + background: ${({ theme }) => theme.colors.bgGray02}; + ${({ theme }) => theme.fonts.body02M}; ` const ReviewTextArea = styled(TextArea)` @@ -128,11 +132,11 @@ const ReviewTextArea = styled(TextArea)` const ReviewSendButton = styled(Button)` height: 64px; margin-top: 40px; - ${({ theme }): string => theme.mediaQuery.tablet} { + ${({ theme }) => theme.mediaQuery.tablet} { height: 48px; } - ${({ theme }): string => theme.mediaQuery.mobile} { + ${({ theme }) => theme.mediaQuery.mobile} { height: 48px; } ` diff --git a/src/components/shop/ReviewModal/types.ts b/src/components/shop/buy/ReviewModal/types.ts similarity index 50% rename from src/components/shop/ReviewModal/types.ts rename to src/components/shop/buy/ReviewModal/types.ts index 72883afc..6b004211 100644 --- a/src/components/shop/ReviewModal/types.ts +++ b/src/components/shop/buy/ReviewModal/types.ts @@ -1,16 +1,15 @@ -import type { ModalProps, IconType } from '@offer-ui/react' +import type { ModalProps } from '@offer-ui/react' +import type { ScoreNames } from '@types' -export type Score = Extract - -export type ScoreState = Score | null +export type ScoreState = ScoreNames | null export type CommonReviewModalProps = Pick & { nickname: string productName: string } -export type SCORE = { - state: Score +export type ScoreOptions = { + state: ScoreNames text: string }[] diff --git a/src/components/shop/buy/index.ts b/src/components/shop/buy/index.ts new file mode 100644 index 00000000..77d19a7c --- /dev/null +++ b/src/components/shop/buy/index.ts @@ -0,0 +1,4 @@ +export * from './LikeTabPost' +export * from './LikeTabPostList' +export * from './OfferTabPost' +export * from './OfferTabPostList' diff --git a/src/components/shop/index.ts b/src/components/shop/index.ts index d0cd9f5e..6699332f 100644 --- a/src/components/shop/index.ts +++ b/src/components/shop/index.ts @@ -1,5 +1,10 @@ -export * from './Post' -export * from './PostList' +export * from './sale' +export * from './buy' +export * from './review' export * from './ProfileBox' export * from './SelectBuyerModal' export * from './EditProfileModal' +export * from './EditProfileModal/types' +export * from './buy/ReviewModal' +export * from './buy/ReviewModal/Write/types' +export * from './buy/ReviewModal/Read/types' diff --git a/src/components/shop/Post/ReviewTabPost/ReviewTabPost.stories.tsx b/src/components/shop/review/ReviewTabPost/ReviewTabPost.stories.tsx similarity index 100% rename from src/components/shop/Post/ReviewTabPost/ReviewTabPost.stories.tsx rename to src/components/shop/review/ReviewTabPost/ReviewTabPost.stories.tsx diff --git a/src/components/shop/Post/ReviewTabPost/index.tsx b/src/components/shop/review/ReviewTabPost/index.tsx similarity index 91% rename from src/components/shop/Post/ReviewTabPost/index.tsx rename to src/components/shop/review/ReviewTabPost/index.tsx index e61008ed..13205baf 100644 --- a/src/components/shop/Post/ReviewTabPost/index.tsx +++ b/src/components/shop/review/ReviewTabPost/index.tsx @@ -4,6 +4,7 @@ import React from 'react' import { Styled } from './styled' import type { ReviewTabPostProps } from './types' import { ICON_META } from './types' +import { getTimeDiffText } from '@utils/format' const ReviewTabPost = ({ reviewTargetMember, @@ -13,8 +14,6 @@ const ReviewTabPost = ({ createdDate, className }: ReviewTabPostProps): ReactElement => { - const offerLevel = 1 - return ( @@ -26,12 +25,11 @@ const ReviewTabPost = ({ {reviewTargetMember.nickname} - {/* Lv.{reviewTargetMember.offerLevel} */} - Lv.{offerLevel} + Lv.{reviewTargetMember.offerLevel} - {createdDate} + {getTimeDiffText(createdDate)} diff --git a/src/components/shop/Post/ReviewTabPost/styled.ts b/src/components/shop/review/ReviewTabPost/styled.ts similarity index 92% rename from src/components/shop/Post/ReviewTabPost/styled.ts rename to src/components/shop/review/ReviewTabPost/styled.ts index 5b0c94aa..75a4a487 100644 --- a/src/components/shop/Post/ReviewTabPost/styled.ts +++ b/src/components/shop/review/ReviewTabPost/styled.ts @@ -1,3 +1,4 @@ +import { css } from '@emotion/react' import styled from '@emotion/styled' import { Avatar as AvatarComponent, @@ -6,13 +7,14 @@ import { } from '@offer-ui/react' const Wrapper = styled.li` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; - padding: 24px; gap: 8px; + padding: 24px; + ${theme.mediaQuery.tablet} { - padding 16px 24px; + padding: 16px 24px; } ${theme.mediaQuery.mobile} { @@ -41,11 +43,13 @@ const NickNameWrapper = styled.div` ` const NickName = styled(Text)` - ${({ theme }): string => ` + ${({ theme }) => css` display: inline-block; + overflow: hidden; + max-width: 350px; + text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; @@ -60,11 +64,13 @@ const Badge = styled(BadgeComponent)` ` const PostTitle = styled(Text)` - ${({ theme }): string => ` + ${({ theme }) => css` display: block; + overflow: hidden; + margin-top: 4px; + text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; diff --git a/src/components/shop/Post/ReviewTabPost/types.ts b/src/components/shop/review/ReviewTabPost/types.ts similarity index 100% rename from src/components/shop/Post/ReviewTabPost/types.ts rename to src/components/shop/review/ReviewTabPost/types.ts diff --git a/src/components/shop/PostList/ReviewTabPostList/ReviewTabPostList.stories.tsx b/src/components/shop/review/ReviewTabPostList/ReviewTabPostList.stories.tsx similarity index 100% rename from src/components/shop/PostList/ReviewTabPostList/ReviewTabPostList.stories.tsx rename to src/components/shop/review/ReviewTabPostList/ReviewTabPostList.stories.tsx diff --git a/src/components/shop/PostList/ReviewTabPostList/index.tsx b/src/components/shop/review/ReviewTabPostList/index.tsx similarity index 91% rename from src/components/shop/PostList/ReviewTabPostList/index.tsx rename to src/components/shop/review/ReviewTabPostList/index.tsx index 6f3f0d00..42b0e3d9 100644 --- a/src/components/shop/PostList/ReviewTabPostList/index.tsx +++ b/src/components/shop/review/ReviewTabPostList/index.tsx @@ -2,7 +2,7 @@ import { Divider } from '@offer-ui/react' import type { ReactElement } from 'react' import { Fragment } from 'react' import type { ReviewTabPostListProps } from './types' -import { ReviewTabPost } from '@components/shop/Post' +import { ReviewTabPost } from '../ReviewTabPost' const ReviewTabPostList = ({ reviews, diff --git a/src/components/shop/PostList/ReviewTabPostList/types.ts b/src/components/shop/review/ReviewTabPostList/types.ts similarity index 100% rename from src/components/shop/PostList/ReviewTabPostList/types.ts rename to src/components/shop/review/ReviewTabPostList/types.ts diff --git a/src/components/shop/review/index.ts b/src/components/shop/review/index.ts new file mode 100644 index 00000000..8ecdf8ef --- /dev/null +++ b/src/components/shop/review/index.ts @@ -0,0 +1,2 @@ +export * from './ReviewTabPost' +export * from './ReviewTabPostList' diff --git a/src/components/shop/Post/SaleTabPost/SaleTabPost.stories.tsx b/src/components/shop/sale/SaleTabPost/SaleTabPost.stories.tsx similarity index 87% rename from src/components/shop/Post/SaleTabPost/SaleTabPost.stories.tsx rename to src/components/shop/sale/SaleTabPost/SaleTabPost.stories.tsx index bc9bced8..bfa70159 100644 --- a/src/components/shop/Post/SaleTabPost/SaleTabPost.stories.tsx +++ b/src/components/shop/sale/SaleTabPost/SaleTabPost.stories.tsx @@ -36,22 +36,17 @@ const PrimaryWithHooks: Story = args => { 내 사용자 - - + + 타 사용자 @@ -59,7 +54,7 @@ const PrimaryWithHooks: Story = args => { } export const Primary: StoryObj = { args: { - hasToken: false, + isLogin: false, // sellerNickName: 'sonny', id: 4, thumbnailImageUrl: '', diff --git a/src/components/shop/Post/SaleTabPost/index.tsx b/src/components/shop/sale/SaleTabPost/index.tsx similarity index 69% rename from src/components/shop/Post/SaleTabPost/index.tsx rename to src/components/shop/sale/SaleTabPost/index.tsx index 7f1ebc17..60a351db 100644 --- a/src/components/shop/Post/SaleTabPost/index.tsx +++ b/src/components/shop/sale/SaleTabPost/index.tsx @@ -1,41 +1,35 @@ -import type { SelectOnChangeHandler } from '@offer-ui/react' import { Icon, Text } from '@offer-ui/react' import type { ReactElement } from 'react' import { Styled } from './styled' import type { SaleTabPostProps } from './types' import { TRADE_STATUS } from '@constants' import type { TradeStatusType } from '@types' -import { toLocaleCurrency } from '@utils' +import { toLocaleCurrency, getTimeDiffText } from '@utils' -const SaleTabPost = (props: SaleTabPostProps): ReactElement => { - const { - className, - id, - title, - price, - thumbnailImageUrl, - tradeStatus, - likeCount, - createdAt, - hasToken, - onChangeTradeStatus - } = props +const SaleTabPost = ({ + className, + id, + title, + price, + thumbnailImageUrl, + tradeStatus, + likeCount, + createdAt, + isLogin, + onChangeTradeStatus, + hasReview +}: SaleTabPostProps): ReactElement => { const isSoldOut = tradeStatus.code === 'SOLD' - const handleChangeTradeStatus: SelectOnChangeHandler< - TradeStatusType - > = item => { + const handleChangeTradeStatus = (item: TradeStatusType) => { onChangeTradeStatus?.(id, item) } - // Sample Value - const isReviewed = false - return ( - + - {hasToken ? ( + {isLogin ? ( { {likeCount} - - {createdAt} + + {getTimeDiffText(createdAt)} - {hasToken && isSoldOut && ( + {isLogin && isSoldOut && ( - {isReviewed ? '보낸 후기 보기' : '후기 보내기'} + {hasReview ? '보낸 후기 보기' : '후기 보내기'} )} diff --git a/src/components/shop/Post/SaleTabPost/styled.ts b/src/components/shop/sale/SaleTabPost/styled.ts similarity index 80% rename from src/components/shop/Post/SaleTabPost/styled.ts rename to src/components/shop/sale/SaleTabPost/styled.ts index d8aa07ce..99b60a16 100644 --- a/src/components/shop/Post/SaleTabPost/styled.ts +++ b/src/components/shop/sale/SaleTabPost/styled.ts @@ -1,3 +1,4 @@ +import { css } from '@emotion/react' import styled from '@emotion/styled' import { Image, @@ -7,7 +8,7 @@ import { } from '@offer-ui/react' const Container = styled.li` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; align-items: center; justify-content: space-between; @@ -19,18 +20,20 @@ const Container = styled.li` } `} ` -const ProductWrapper = styled.div<{ hasToken: boolean }>` - ${({ theme, hasToken }): string => ` +const ProductWrapper = styled.div<{ isLogin: boolean }>` + ${({ theme, isLogin }) => css` display: grid; flex: 1; - grid-template-columns: ${hasToken ? '90px 90px 1fr' : '90px 1fr'}; - align-items: center; + grid-template-columns: ${isLogin ? '90px 90px 1fr' : '90px 1fr'}; gap: 16px; + align-items: center; + padding: 20px 0 20px 20px; ${theme.mediaQuery.tablet} { grid-template-columns: 68px 1fr 90px; gap: 8px; + padding: 16px 24px; } @@ -41,10 +44,11 @@ const ProductWrapper = styled.div<{ hasToken: boolean }>` `} ` const ProductImg = styled(Image)` - ${({ theme }): string => ` + ${({ theme }) => css` + order: 1; + width: 90px; height: 90px; - order:1; ${theme.mediaQuery.tablet} { width: 68px; @@ -53,7 +57,7 @@ const ProductImg = styled(Image)` `} ` const SelectBox = styled(SelectBoxComponent)` - ${({ theme }): string => ` + ${({ theme }) => css` order: 2; ${theme.mediaQuery.tablet} { @@ -63,32 +67,35 @@ const SelectBox = styled(SelectBoxComponent)` ` const ProductMetaWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; align-items: center; justify-content: space-around; order: 3; ${theme.mediaQuery.tablet} { - align-items: flex-start; flex-direction: column; - order: 2; gap: 4px; + align-items: flex-start; + order: 2; } `} ` const ProductName = styled(Text)` - ${({ theme }): string => ` - text-align: center; + ${({ theme }) => css` + overflow: hidden; + width: 150px; + + text-align: center; text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; word-break: break-word; ${theme.mediaQuery.tablet} { - text-align: left; width: 460px; + + text-align: left; } ${theme.mediaQuery.mobile} { @@ -97,21 +104,21 @@ const ProductName = styled(Text)` `} ` const ProductInfoWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; + gap: 12px; align-items: center; justify-content: space-around; - gap: 12px; ${theme.mediaQuery.tablet} { - align-items: flex-start; flex-direction: column; gap: 0; + align-items: flex-start; } -`} + `} ` const Price = styled.span` - ${({ theme }): string => ` + ${({ theme }) => css` ${theme.fonts.body02R}; ${theme.mediaQuery.tablet} { @@ -121,12 +128,14 @@ const Price = styled.span` `} ` const FavoriteWrapper = styled.div<{ isOnlyOther: boolean }>` - ${({ theme, isOnlyOther }): string => ` + ${({ theme, isOnlyOther }) => css` display: ${isOnlyOther ? 'none' : 'flex'}; + gap: 2px; align-items: center; justify-content: center; - gap: 2px; + width: 100px; + color: ${theme.colors.grayScale50}; ${theme.mediaQuery.tablet} { @@ -135,43 +144,46 @@ const FavoriteWrapper = styled.div<{ isOnlyOther: boolean }>` } `} ` -const Date = styled(Text)<{ hasToken: boolean }>` - ${({ theme, hasToken }): string => ` +const Date = styled(Text)<{ isLogin: boolean }>` + ${({ theme, isLogin }) => css` display: inline-block; + color: ${theme.colors.grayScale50}; ${theme.mediaQuery.tablet} { - display: ${hasToken ? 'none' : 'inline-block'}; + display: ${isLogin ? 'none' : 'inline-block'}; ${theme.fonts.caption01M}; color: ${theme.colors.grayScale30}; } `} ` const ReviewButtonWrapper = styled.div` - ${({ theme }): string => ` + ${({ theme }) => css` display: flex; flex-direction: column; - min-width: 120px; align-items: flex-end; + min-width: 120px; + ${theme.mediaQuery.tablet} { display: flex; align-items: center; } `} ` -const ReviewButton = styled(Button)<{ isReviewed: boolean }>` - ${({ theme, isReviewed }): string => ` - color: ${isReviewed ? theme.colors.grayScale70 : theme.colors.brandPrimary}; +const ReviewButton = styled(Button)<{ hasReview: boolean }>` + ${({ theme, hasReview }) => css` margin-right: 20px; + color: ${hasReview ? theme.colors.grayScale70 : theme.colors.brandPrimary}; + ${theme.mediaQuery.tablet} { width: 100%; + margin-right: 0; + padding: 20px 0; border: none; border-top: 1px solid ${theme.colors.grayScale10}; border-radius: 0; - padding: 20px 0; - margin-right: 0; } `} ` diff --git a/src/components/shop/Post/SaleTabPost/types.ts b/src/components/shop/sale/SaleTabPost/types.ts similarity index 92% rename from src/components/shop/Post/SaleTabPost/types.ts rename to src/components/shop/sale/SaleTabPost/types.ts index 4ec29735..43872626 100644 --- a/src/components/shop/Post/SaleTabPost/types.ts +++ b/src/components/shop/sale/SaleTabPost/types.ts @@ -1,7 +1,7 @@ import type { PostSummary, TradeStatusType } from '@types' export type SaleTabPostProps = { - hasToken: boolean + isLogin: boolean className?: string // tradeStatus 변경 시, 이벤트 onChangeTradeStatus(productId: number, status: TradeStatusType): void diff --git a/src/components/shop/PostList/SaleTabPostList/SaleTabPostList.stories.tsx b/src/components/shop/sale/SaleTabPostList/SaleTabPostList.stories.tsx similarity index 94% rename from src/components/shop/PostList/SaleTabPostList/SaleTabPostList.stories.tsx rename to src/components/shop/sale/SaleTabPostList/SaleTabPostList.stories.tsx index e0434b7d..b8bc57bd 100644 --- a/src/components/shop/PostList/SaleTabPostList/SaleTabPostList.stories.tsx +++ b/src/components/shop/sale/SaleTabPostList/SaleTabPostList.stories.tsx @@ -48,14 +48,14 @@ const PrimaryWithHooks = (args: SaleTabPostListProps) => {
{tradeStatus.name}
- + ) } export const Primary: StoryObj = { args: { - hasToken: true, + isLogin: true, posts: [], onChangeTradeStatus: (id: number, status: TradeStatusType): void => { action('onChangeTradeStatus')(id, status) diff --git a/src/components/shop/PostList/SaleTabPostList/index.tsx b/src/components/shop/sale/SaleTabPostList/index.tsx similarity index 87% rename from src/components/shop/PostList/SaleTabPostList/index.tsx rename to src/components/shop/sale/SaleTabPostList/index.tsx index e53802e0..4a0c2e2e 100644 --- a/src/components/shop/PostList/SaleTabPostList/index.tsx +++ b/src/components/shop/sale/SaleTabPostList/index.tsx @@ -2,11 +2,11 @@ import { Divider } from '@offer-ui/react' import type { ReactElement } from 'react' import { Fragment } from 'react' import type { SaleTabPostListProps } from './types' -import { SaleTabPost } from '@components/shop/Post' +import { SaleTabPost } from '../SaleTabPost' const SaleTabPostList = ({ posts, - hasToken, + isLogin, onChangeTradeStatus, className }: SaleTabPostListProps): ReactElement => { @@ -16,7 +16,7 @@ const SaleTabPostList = ({ {index !== posts.length - 1 && } diff --git a/src/components/shop/PostList/SaleTabPostList/types.ts b/src/components/shop/sale/SaleTabPostList/types.ts similarity index 70% rename from src/components/shop/PostList/SaleTabPostList/types.ts rename to src/components/shop/sale/SaleTabPostList/types.ts index 274e5f3d..e760eb03 100644 --- a/src/components/shop/PostList/SaleTabPostList/types.ts +++ b/src/components/shop/sale/SaleTabPostList/types.ts @@ -1,9 +1,9 @@ -import type { SaleTabPostProps } from '@components/shop/Post/SaleTabPost' +import type { SaleTabPostProps } from '@components/shop/sale/SaleTabPost' import type { PostSummary } from '@types' export type SaleTabPostListProps = { posts: PostSummary[] className?: string - hasToken: boolean + isLogin: boolean onChangeTradeStatus: SaleTabPostProps['onChangeTradeStatus'] } diff --git a/src/components/shop/sale/index.ts b/src/components/shop/sale/index.ts new file mode 100644 index 00000000..67895943 --- /dev/null +++ b/src/components/shop/sale/index.ts @@ -0,0 +1,2 @@ +export * from './SaleTabPost' +export * from './SaleTabPostList' diff --git a/src/constants/app.ts b/src/constants/app.ts index f69922df..2ea307f1 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -79,9 +79,9 @@ export const TRADE_ACTIVITY_TYPES = { offer: '가격제안' }, review: { - all: '전체후기', - buyer: '구매자 후기', - seller: '판매자 후기' + ALL: '전체후기', + BUYER: '구매자 후기', + SELLER: '판매자 후기' } } as const @@ -135,3 +135,9 @@ export const CATEGORIES = [ name: '기타 중고물품' } ] as const + +export const SCORE = { + sad: 0, + meh: 1, + smile: 2 +} as const diff --git a/src/constants/index.ts b/src/constants/index.ts index 0c4403eb..2d17d74e 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -2,3 +2,4 @@ export * from './icons' export * from './images' export * from './app' export * from './env' +export * from './message' diff --git a/src/constants/message.ts b/src/constants/message.ts new file mode 100644 index 00000000..272a42a8 --- /dev/null +++ b/src/constants/message.ts @@ -0,0 +1,6 @@ +export const VALID_NICKNAME_MESSAGE = { + SUCCESS: '사용할 수 있는 닉네임입니다.', + DUPLICATED_ERROR: '이미 사용 중인 닉네임입니다.', + MIN_LENGTH_ERROR: '닉네임은 2자 이상 입력해주세요.', + EMPTY_ERROR: '닉네임을 입력해주세요!' +} as const diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 0bbe63da..e106687c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,4 +1,3 @@ export * from './useResponsive' export * from './useAuth' export * from './useModal' -export * from './useProfile' diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 976bbe21..edf04ece 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -7,8 +7,8 @@ import { env } from '@constants' export const useAuth = () => { const router = useRouter() const accessToken = getCookie(env.AUTH_TOKEN_KEY) - const user = useGetMyProfileQuery(accessToken) - const [isLogin, setIsLogin] = useState(false) + const getMyProfileQuery = useGetMyProfileQuery(accessToken) + const [isLogin, setIsLogin] = useState(getMyProfileQuery.isSuccess) const handleLogout = () => { deleteCookie(env.AUTH_TOKEN_KEY) @@ -23,8 +23,8 @@ export const useAuth = () => { return { isLogin, - isLoading: user.isLoading, + isLoading: getMyProfileQuery.isLoading, handleLogout, - user: user.data + user: getMyProfileQuery.data } } diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts deleted file mode 100644 index 10fcdd73..00000000 --- a/src/hooks/useProfile.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getCookie } from 'cookies-next' -import { useGetMyProfileQuery, useGetMemberProfileQuery } from '@apis' -import type { GetMyProfileRes, GetMemberProfileRes } from '@apis' -import { env } from '@constants' - -type UseProfileReturns = { profile: GetMyProfileRes | GetMemberProfileRes } - -export const useProfile = (memberId = ''): UseProfileReturns => { - const token = getCookie(env.AUTH_TOKEN_KEY) - - const myProfile = useGetMyProfileQuery(token) - const memberProfile = useGetMemberProfileQuery(memberId) - - const isMyProfile = myProfile && memberId === String(myProfile.data?.id) - const profile = isMyProfile ? myProfile.data : memberProfile.data - - return { profile } -} diff --git a/src/hooks/useValidateNickname.ts b/src/hooks/useValidateNickname.ts new file mode 100644 index 00000000..8591397a --- /dev/null +++ b/src/hooks/useValidateNickname.ts @@ -0,0 +1,45 @@ +import { useCheckValidNicknameMutation } from '@apis' +import { VALID_NICKNAME_MESSAGE } from '@constants' + +export const useValidateNickname = () => { + const checkValidNickname = useCheckValidNicknameMutation() + + const validateNickname = async (nickname: string) => { + if (nickname.length === 0) { + return { + isSuccess: false, + message: VALID_NICKNAME_MESSAGE.EMPTY_ERROR + } + } + + if (nickname.length < 2) { + return { + isSuccess: false, + message: VALID_NICKNAME_MESSAGE.MIN_LENGTH_ERROR + } + } + + try { + const { duplicate } = await checkValidNickname.mutateAsync(nickname) + + if (duplicate) { + return { + isSuccess: false, + message: VALID_NICKNAME_MESSAGE.DUPLICATED_ERROR + } + } else { + return { + isSuccess: true, + message: VALID_NICKNAME_MESSAGE.SUCCESS + } + } + } catch (e) { + return { + isSuccess: false, + message: 'An error occurred during nickname validation.' + } + } + } + + return validateNickname +} diff --git a/src/pages/shop/[id].tsx b/src/pages/shop/[id].tsx index bf9095b1..b1450957 100644 --- a/src/pages/shop/[id].tsx +++ b/src/pages/shop/[id].tsx @@ -1,23 +1,25 @@ import type { GetServerSidePropsContext } from 'next' import type { ReactElement } from 'react' -import { ShopPageMainView } from './view/main' -import { useProfile } from '@hooks' +import { ShopPageView } from './view' +import type { TradeActivityCodes } from '@types' + +export const getServerSideProps = ({ query }: GetServerSidePropsContext) => { + const { id, tab = 'sale' } = query -export const getServerSideProps = (context: GetServerSidePropsContext) => { return { props: { - memberId: context.query.id + memberId: id, + currentTab: tab } } } type ShopPageProps = { memberId: string + currentTab: TradeActivityCodes } -const ShopPage = ({ memberId }: ShopPageProps): ReactElement => { - const { profile } = useProfile(memberId) - - return +const ShopPage = ({ memberId, currentTab }: ShopPageProps): ReactElement => { + return } export default ShopPage diff --git a/src/pages/shop/index.tsx b/src/pages/shop/index.tsx index 5e83d435..b8656310 100644 --- a/src/pages/shop/index.tsx +++ b/src/pages/shop/index.tsx @@ -1,10 +1,21 @@ -import { ShopPageMainView } from './view/main' -import { useAuth } from '@hooks/useAuth' +import type { GetServerSidePropsContext } from 'next/types' +import { ShopPageView } from './view' +import type { TradeActivityCodes } from '@types' -const MyShopPage = () => { - const { user } = useAuth() +export const getServerSideProps = ({ query }: GetServerSidePropsContext) => { + const { tab = 'sale' } = query - return + return { + props: { + currentTab: tab + } + } +} +type MyShopPageProps = { + currentTab: TradeActivityCodes +} +const MyShopPage = ({ currentTab }: MyShopPageProps) => { + return } export default MyShopPage diff --git a/src/pages/shop/pageTabs.ts b/src/pages/shop/pageTabs.ts index 2e59a8b1..227c707e 100644 --- a/src/pages/shop/pageTabs.ts +++ b/src/pages/shop/pageTabs.ts @@ -1,10 +1,9 @@ import type { ReactElement } from 'react' -import type { ShopPageBuyViewProps } from './view/buy' -import { ShopPageBuyView } from './view/buy' -import type { ShopPageReviewViewProps } from './view/review' -import { ShopPageReviewView } from './view/review' -import type { ShopPageSaleViewProps } from './view/sale' -import { ShopPageSaleView } from './view/sale' +import { ShopPageBuyPanel } from './panel/buy' +import type { ShopPageReviewPanelProps } from './panel/review' +import { ShopPageReviewPanel } from './panel/review' +import type { ShopPageSalePanelProps } from './panel/sale' +import { ShopPageSalePanel } from './panel/sale' import type { TradeActivityNames, TradeActivityCodes } from '@types' type PageTab = { @@ -15,26 +14,28 @@ type PageTab = { panel(props: unknown): ReactElement } +export const tabList = ['sale', 'buy', 'review'] + export const pageTabs: PageTab[] = [ { tab: { code: 'sale', name: '판매' }, - panel: (props: ShopPageSaleViewProps) => ShopPageSaleView(props) + panel: (props: ShopPageSalePanelProps) => ShopPageSalePanel(props) }, { tab: { code: 'buy', name: '구매' }, - panel: (props: ShopPageBuyViewProps) => ShopPageBuyView(props) + panel: ShopPageBuyPanel }, { tab: { code: 'review', name: '후기' }, - panel: (props: ShopPageReviewViewProps) => ShopPageReviewView(props) + panel: (props: ShopPageReviewPanelProps) => ShopPageReviewPanel(props) } ] diff --git a/src/pages/shop/panel/buy/index.tsx b/src/pages/shop/panel/buy/index.tsx new file mode 100644 index 00000000..8532afbc --- /dev/null +++ b/src/pages/shop/panel/buy/index.tsx @@ -0,0 +1,82 @@ +import { SelectBox } from '@offer-ui/react' +import { useState } from 'react' +import { Styled } from './styled' +import { LikePanelView } from './view/like' +import { OfferPanelView } from './view/offer' +import { useGetMyOffersQuery, useGetLikedPostsQuery } from '@apis' +import { Tabs } from '@components' +import { TRADE_ACTIVITY_TYPES, SORT_OPTIONS } from '@constants' +import type { + SortOption, + SortOptionCodes, + TradeBuyActivityCodes, + TradeBuyActivityNames +} from '@types' + +const tradeBuyActivityTabs = Object.entries< + TradeBuyActivityCodes, + TradeBuyActivityNames +>(TRADE_ACTIVITY_TYPES.buy) + +export const ShopPageBuyPanel = () => { + const [sortOptionCode, setSortOptionCode] = + useState('CREATED_DATE_DESC') + const [activityType, setActivityType] = + useState('like') + + const offers = useGetMyOffersQuery({ sort: sortOptionCode }) + const likedPosts = useGetLikedPostsQuery({ sort: sortOptionCode }) + + const handleChangeSortOption = (newSortOption: SortOption) => { + setSortOptionCode(newSortOption.code) + } + + return ( +
+ + + + + + {tradeBuyActivityTabs.map(([code, name]) => { + const isCurrent = code === activityType + + return ( + setActivityType(code)}> + + + + {name} + + + {code === 'like' + ? likedPosts.data?.posts.length + : offers.data?.offers.length} + + + + ) + })} + + + + + + + + + + + + + +
+ ) +} diff --git a/src/pages/shop/view/buy/styled.ts b/src/pages/shop/panel/buy/styled.ts similarity index 97% rename from src/pages/shop/view/buy/styled.ts rename to src/pages/shop/panel/buy/styled.ts index 599a038f..b92be3d6 100644 --- a/src/pages/shop/view/buy/styled.ts +++ b/src/pages/shop/panel/buy/styled.ts @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import type { ColorKeys } from '@offer-ui/react' import { Divider as DividerComponent } from '@offer-ui/react' -import { Tabs, Tab as TabComponent } from '@components/common' +import { Tabs, Tab as TabComponent } from '@components' const SearchOptionsWrapper = styled.div` ${({ theme }): string => ` diff --git a/src/pages/shop/panel/buy/view/like.tsx b/src/pages/shop/panel/buy/view/like.tsx new file mode 100644 index 00000000..9c85beca --- /dev/null +++ b/src/pages/shop/panel/buy/view/like.tsx @@ -0,0 +1,23 @@ +import { useGetLikedPostsQuery, useUpdateLikeStatusMutation } from '@apis' +import { LikeTabPostList } from '@components' +import type { SortOptionCodes } from '@types' + +type LikePanelViewProps = { + sortOptionCode: SortOptionCodes +} +export const LikePanelView = ({ sortOptionCode }: LikePanelViewProps) => { + const likedPosts = useGetLikedPostsQuery({ sort: sortOptionCode }) + const likeStatusMutation = useUpdateLikeStatusMutation() + + const handleChangeProductLikeStatus = async (postId: number) => { + await likeStatusMutation.mutateAsync(postId) + likedPosts.refetch() + } + + return ( + + ) +} diff --git a/src/pages/shop/panel/buy/view/offer.tsx b/src/pages/shop/panel/buy/view/offer.tsx new file mode 100644 index 00000000..c76acad6 --- /dev/null +++ b/src/pages/shop/panel/buy/view/offer.tsx @@ -0,0 +1,75 @@ +import { useState } from 'react' +import { useModal } from '@hooks/useModal' +import { + useGetMyOffersQuery, + useGetReviewsCountsQuery, + useReviewsMutation +} from '@apis' +import type { ReviewState } from '@components' +import { OfferTabPostList, ReviewModal } from '@components' +import { SCORE } from '@constants' +import { useAuth } from '@hooks' +import type { OfferSummary, Review, SortOptionCodes } from '@types' + +type OfferPanelViewProps = { + sortOptionCode: SortOptionCodes +} +export const OfferPanelView = ({ sortOptionCode }: OfferPanelViewProps) => { + const { user } = useAuth() + const offers = useGetMyOffersQuery({ sort: sortOptionCode }) + const reviewsCounts = useGetReviewsCountsQuery(user.id) + const reviewsMutation = useReviewsMutation() + + const readReviewModal = useModal() + const writeReviewModal = useModal() + + const [readReviewProps, setReadReviewProps] = useState({} as Review) + const [writeReviewProps, setWriteReviewProps] = useState({} as OfferSummary) + + const handleOpenReadReview = (review: Review) => { + setReadReviewProps(review) + readReviewModal.openModal() + } + const handleOpenWriteReview = (offer: OfferSummary) => { + setWriteReviewProps(offer) + writeReviewModal.openModal() + } + const handleConfirmWriteReview = async (reviewState: ReviewState) => { + if (!reviewState.reviewScore) { + return + } + + await reviewsMutation.mutateAsync({ + targetMemberId: writeReviewProps.seller.id, + postId: writeReviewProps.postId, + score: SCORE[reviewState.reviewScore], + content: reviewState.reviewText + }) + await offers.refetch() + await reviewsCounts.refetch() + writeReviewModal.closeModal() + } + + return ( + <> + + + + + ) +} diff --git a/src/pages/shop/panel/review/index.tsx b/src/pages/shop/panel/review/index.tsx new file mode 100644 index 00000000..4ae9e614 --- /dev/null +++ b/src/pages/shop/panel/review/index.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import { Styled } from './styled' +import { + useGetProfileQuery, + useGetReviewsCountsQuery, + useGetReviewsQuery +} from '@apis' +import { Tabs, ReviewTabPostList } from '@components' +import { TRADE_ACTIVITY_TYPES } from '@constants' +import type { TradeReviewActivityCodes, TradeReviewActivityNames } from '@types' + +const tradeReviewActivityList = Object.entries< + TradeReviewActivityCodes, + TradeReviewActivityNames +>(TRADE_ACTIVITY_TYPES.review) + +export type ShopPageReviewPanelProps = { + memberId: number | null +} + +export const ShopPageReviewPanel = ({ memberId }: ShopPageReviewPanelProps) => { + const [reviewType, setReviewType] = useState('ALL') + + const profile = useGetProfileQuery(memberId) + const reviewsCounts = useGetReviewsCountsQuery(profile.data.id) + const reviews = useGetReviewsQuery({ + memberId: profile.data.id, + lastId: 0, + limit: 100, + role: reviewType + }) + + const handleChangeReviewType = + (newReviewType: TradeReviewActivityCodes) => () => { + setReviewType(newReviewType) + } + + return ( +
+ + + + + + {tradeReviewActivityList.map(([code, name]) => { + const isCurrent = code === reviewType + + return ( + + + + + {name} + + + {reviewsCounts.data[code]} + + + + ) + })} + + + + {tradeReviewActivityList.map(([code]) => ( + + + + ))} + + + +
+ ) +} diff --git a/src/pages/shop/view/review/styled.ts b/src/pages/shop/panel/review/styled.ts similarity index 97% rename from src/pages/shop/view/review/styled.ts rename to src/pages/shop/panel/review/styled.ts index acd7f726..7c0fea9c 100644 --- a/src/pages/shop/view/review/styled.ts +++ b/src/pages/shop/panel/review/styled.ts @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import type { ColorKeys } from '@offer-ui/react' import { Divider as DividerComponent } from '@offer-ui/react' -import { Tabs, Tab as TabComponent } from '@components/common' +import { Tabs, Tab as TabComponent } from '@components' const SearchOptionsWrapper = styled.div` ${({ theme }): string => ` diff --git a/src/pages/shop/panel/sale/index.tsx b/src/pages/shop/panel/sale/index.tsx new file mode 100644 index 00000000..6850036b --- /dev/null +++ b/src/pages/shop/panel/sale/index.tsx @@ -0,0 +1,122 @@ +import { SelectBox } from '@offer-ui/react' +import { useEffect, useState } from 'react' +import { Styled } from './styled' +import type { GetPostsReq } from '@apis' +import { + useUpdateTradeStatusMutation, + useGetPostsQuery, + useGetProfileQuery +} from '@apis' +import type { SaleTabPostProps } from '@components' +import { Tabs, SaleTabPostList } from '@components' +import { SORT_OPTIONS, TRADE_STATUS } from '@constants' +import type { SortOption } from '@types' + +export type ShopPageSalePanelProps = { + isLogin: boolean + memberId: number | null +} +export const ShopPageSalePanel = ({ + isLogin, + memberId +}: ShopPageSalePanelProps) => { + const [searchOptions, setSearchOptions] = useState({ + sellerId: memberId ? memberId : undefined, + tradeStatus: 'SELLING', + sort: 'CREATED_DATE_DESC' + }) + + const profile = useGetProfileQuery(memberId) + const posts = useGetPostsQuery(searchOptions) + const updateTradeStatus = useUpdateTradeStatusMutation() + + useEffect( + function fetchPostsOnMount() { + if (!profile.data.id) { + return + } + + setSearchOptions(prevOptions => ({ + ...prevOptions, + sellerId: profile.data.id + })) + }, + [profile.data.id] + ) + + const handleChangeSearchOptions = (newOption: GetPostsReq) => + setSearchOptions({ + ...searchOptions, + ...newOption + }) + const handleChangeProductTradeStatus: SaleTabPostProps['onChangeTradeStatus'] = + async (postId, tradeStatus) => { + await updateTradeStatus.mutateAsync({ + postId, + tradeStatus: tradeStatus.code + }) + + await posts.refetch() + profile.refetch() + } + + return ( +
+ + + + + + {TRADE_STATUS.map(tradeStatus => { + const isCurrent = tradeStatus.code === searchOptions.tradeStatus + + return ( + + handleChangeSearchOptions({ + tradeStatus: tradeStatus.code + }) + }> + + + + + {tradeStatus.name} + + + + {tradeStatus.code === 'SELLING' + ? profile.data.sellingProductCount + : profile.data.soldProductCount} + + + + ) + })} + + + handleChangeSearchOptions({ sort: option.code }) + } + /> + + + {TRADE_STATUS.map(tradeStatus => ( + + + + ))} + + + +
+ ) +} diff --git a/src/pages/shop/view/sale/styled.ts b/src/pages/shop/panel/sale/styled.ts similarity index 97% rename from src/pages/shop/view/sale/styled.ts rename to src/pages/shop/panel/sale/styled.ts index 599a038f..b92be3d6 100644 --- a/src/pages/shop/view/sale/styled.ts +++ b/src/pages/shop/panel/sale/styled.ts @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import type { ColorKeys } from '@offer-ui/react' import { Divider as DividerComponent } from '@offer-ui/react' -import { Tabs, Tab as TabComponent } from '@components/common' +import { Tabs, Tab as TabComponent } from '@components' const SearchOptionsWrapper = styled.div` ${({ theme }): string => ` diff --git a/src/pages/shop/view/buy/index.tsx b/src/pages/shop/view/buy/index.tsx deleted file mode 100644 index 6977ec22..00000000 --- a/src/pages/shop/view/buy/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { SelectBox } from '@offer-ui/react' -import type { ReactElement, MouseEvent } from 'react' -import { useState } from 'react' -import { Styled } from './styled' -import { sortItems } from './types' -import { Tabs } from '@components/common' -import { BuyTabPostList } from '@components/shop/PostList' -import { TRADE_ACTIVITY_TYPES } from '@constants' -import type { TradeBuyActivityCodes, TradeBuyActivityNames } from '@types' -import { noop } from '@utils' - -const tradeBuyActivityList = Object.entries< - TradeBuyActivityCodes, - TradeBuyActivityNames ->(TRADE_ACTIVITY_TYPES.buy) - -const getArticles = () => { - return [] -} - -export type ShopPageBuyViewProps = { - memberId: number -} - -export const ShopPageBuyView = ({ - memberId -}: ShopPageBuyViewProps): ReactElement => { - const [tabIndex, setTabIndex] = useState(0) - const [articles] = useState(getArticles()) - - // eslint-disable-next-line no-console - console.log(memberId) - - const handleTabClick = ( - e: MouseEvent, - index: number - ): void => { - setTabIndex(index) - } - - return ( -
- - - - - - {tradeBuyActivityList.map((tradeBuyActivity, index) => { - const isCurrent = tabIndex === index - - return ( - - - - - - {tradeBuyActivity[1]} - - - 1 - - - ) - })} - - - - - - - - - - - - - -
- ) -} diff --git a/src/pages/shop/view/buy/types.ts b/src/pages/shop/view/buy/types.ts deleted file mode 100644 index 60574f5c..00000000 --- a/src/pages/shop/view/buy/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const sortItems = [ - { - code: 'recently', - name: '최신순' - } -] diff --git a/src/pages/shop/view/index.tsx b/src/pages/shop/view/index.tsx new file mode 100644 index 00000000..2c73509f --- /dev/null +++ b/src/pages/shop/view/index.tsx @@ -0,0 +1,129 @@ +import { Divider } from '@offer-ui/react' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { Styled } from './styled' +import { pageTabs, tabList } from '../pageTabs' +import { useValidateNickname } from '@hooks/useValidateNickname' +import { + useCreateUploadImagesMutation, + useGetProfileQuery, + useUpdateMyProfileMutation +} from '@apis' +import type { EditProfileForm } from '@components' +import { EditProfileModal, ProfileBox, Tabs } from '@components' +import { useModal } from '@hooks' +import type { TradeActivityCodes } from '@types' +import { isNumber } from '@utils' + +const initialEditProfileValidate = { + isSuccess: false, + message: '' +} + +type ShopPageViewProps = { + memberId: number | null + currentTab: TradeActivityCodes +} +export const ShopPageView = ({ memberId, currentTab }: ShopPageViewProps) => { + const defaultTabIndex = tabList.findIndex(tab => tab === currentTab) + const [currentPage, setCurrentPage] = useState(currentTab) + const [editProfileValidate, setEditProfileValidate] = useState( + initialEditProfileValidate + ) + + const profile = useGetProfileQuery(memberId) + const createUploadImage = useCreateUploadImagesMutation() + const updateMyProfile = useUpdateMyProfileMutation() + + const isLogin = !isNumber(memberId) + const profileModal = useModal() + const router = useRouter() + const validateNickname = useValidateNickname() + + const handleChangePage = (code: TradeActivityCodes) => (): void => { + router.push(`${router.pathname}?tab=${code}`) + setCurrentPage(code) + } + + const handleValidateNickname = async (nickname: string) => { + const validate = await validateNickname(nickname) + setEditProfileValidate(validate) + } + const handleChangeProfileImage = async (image: EditProfileForm['image']) => { + if (!image.file) { + return image + } + + const imageFormData = new FormData() + imageFormData.append('files', image.file) + const { imageUrls } = await createUploadImage.mutateAsync(imageFormData) + + return { id: image.id, file: image.file, url: imageUrls[0] } + } + + const handleCloseEditProfileModal = () => { + setEditProfileValidate(initialEditProfileValidate) + profileModal.closeModal() + } + const handleConfirmEditProfile = async (profileForm: EditProfileForm) => { + await updateMyProfile.mutateAsync({ + memberId: profile.data.id, + nickname: profileForm.nickname, + profileImageUrl: profileForm.image.url + }) + await profile.refetch() + + handleCloseEditProfileModal() + } + + return ( +
+ {profile.data.nickname}님의 거래 활동 + + + + + {pageTabs + .filter(pageTab => (isLogin ? true : pageTab.tab.code !== 'buy')) + .map(({ tab }) => ( + + {tab.name} + + ))} + + + + + + {pageTabs + .filter(pageTab => (isLogin ? true : pageTab.tab.code !== 'buy')) + .map(({ tab, panel }) => ( + + + + {panel({ isLogin, memberId })} + + + ))} + + + + + +
+ ) +} diff --git a/src/pages/shop/view/main/index.tsx b/src/pages/shop/view/main/index.tsx deleted file mode 100644 index e4d878fe..00000000 --- a/src/pages/shop/view/main/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Divider } from '@offer-ui/react' -import type { MouseEvent } from 'react' -import { useState } from 'react' -import { Styled } from './styled' -import { pageTabs } from '../../pageTabs' -import { ProfileBox, Tabs } from '@components' -import type { MyProfile, MemberProfile } from '@types' - -type ShopPageMainViewProps = { - profile: MyProfile | MemberProfile -} -export const ShopPageMainView = ({ profile }: ShopPageMainViewProps) => { - const [pageIndex, setPageIndex] = useState(0) - - const handleTabClick = ( - e: MouseEvent, - index: number - ): void => { - setPageIndex(index) - } - - return ( -
- {profile.nickname}님의 거래 활동 - - - - - {pageTabs.map(({ tab }, index) => ( - - {tab.name} - - ))} - - - - - - {pageTabs.map(({ tab, panel }) => ( - - - - {panel({ memberId: profile.id })} - - - ))} - - - - -
- ) -} diff --git a/src/pages/shop/view/review/index.tsx b/src/pages/shop/view/review/index.tsx deleted file mode 100644 index 754088cf..00000000 --- a/src/pages/shop/view/review/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { ReactElement, MouseEvent } from 'react' -import { useState } from 'react' -import { Styled } from './styled' -import { Tabs } from '@components/common' -import { ReviewTabPostList } from '@components/shop/PostList' -import { TRADE_ACTIVITY_TYPES } from '@constants' -import type { TradeReviewActivityCodes, TradeReviewActivityNames } from '@types' - -const tradeReviewActivityList = Object.entries< - TradeReviewActivityCodes, - TradeReviewActivityNames ->(TRADE_ACTIVITY_TYPES.review) - -const getReviews = () => { - return [] -} - -export type ShopPageReviewViewProps = { - memberId: number -} - -export const ShopPageReviewView = ({ - memberId -}: ShopPageReviewViewProps): ReactElement => { - const [tabIndex, setTabIndex] = useState(0) - const [reviews] = useState(getReviews()) - - // eslint-disable-next-line no-console - console.log(memberId) - - const handleTabClick = ( - e: MouseEvent, - index: number - ): void => { - setTabIndex(index) - } - - return ( -
- - - - - - {tradeReviewActivityList.map((tradeReviewActivity, index) => { - const isCurrent = tabIndex === index - - return ( - - - - - - {tradeReviewActivity[1]} - - - 1 - - - ) - })} - - - - - - - - - - - - - - - -
- ) -} diff --git a/src/pages/shop/view/sale/index.tsx b/src/pages/shop/view/sale/index.tsx deleted file mode 100644 index d7e0e755..00000000 --- a/src/pages/shop/view/sale/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { SelectBox } from '@offer-ui/react' -import type { ReactElement } from 'react' -import { useState } from 'react' -import { Styled } from './styled' -import type { GetPostsReq } from '@apis/post' -import { useGetPostsQuery } from '@apis/post' -import { Tabs } from '@components/common' -import { SaleTabPostList } from '@components/shop/PostList' -import { SORT_OPTIONS, TRADE_STATUS } from '@constants' -import type { TradeStatusCodes } from '@types' -import { noop } from '@utils' - -export type ShopPageSaleViewProps = { - memberId: number -} -export const ShopPageSaleView = ({ - memberId -}: ShopPageSaleViewProps): ReactElement => { - const hasToken = true - const [tradeStatusCode, setTradeStatusCode] = - useState('SELLING') - - const searchOptions = { - sellerId: memberId, - tradeStatus: tradeStatusCode - } as GetPostsReq - - const { data: postsInfo } = useGetPostsQuery(searchOptions) - - const handleTabClick = (newTradeStatusCode: TradeStatusCodes) => () => { - setTradeStatusCode(newTradeStatusCode) - } - - return ( -
- - - - - - {TRADE_STATUS.map(tradeStatus => { - const isCurrent = tradeStatus.code === tradeStatusCode - - return ( - - - - - - {tradeStatus.name} - - - - {/* MEMO: 거래 상태 갯수 */} - {postsInfo?.posts.length} - - - - ) - })} - - - - - - - - - - - - - -
- ) -} diff --git a/src/pages/shop/view/main/styled.tsx b/src/pages/shop/view/styled.tsx similarity index 93% rename from src/pages/shop/view/main/styled.tsx rename to src/pages/shop/view/styled.tsx index 62442481..c7529735 100644 --- a/src/pages/shop/view/main/styled.tsx +++ b/src/pages/shop/view/styled.tsx @@ -12,11 +12,7 @@ const UserName = styled.p` ${theme.mediaQuery.tablet} { ${theme.fonts.body01B}; - margin: 16px auto; - } - - ${theme.mediaQuery.mobile} { - margin-left: 16px; + margin: 16px; } `} ` diff --git a/src/types/scheme.ts b/src/types/scheme.ts index a364f61f..9a69f02f 100644 --- a/src/types/scheme.ts +++ b/src/types/scheme.ts @@ -82,9 +82,6 @@ export type MyProfile = MemberProfile & { export type ImagesUpload = { imageUrls: string[] } -export type ImageUpload = { - imageUrl: string -} /** Review */ export type ReviewInfo = { @@ -99,6 +96,11 @@ export type Review = { content: string createdDate: string } +export type ReviewCount = { + all: number + seller: number + buyer: number +} export type ReviewTargetMember = { id: number profileImageUrl: string diff --git a/src/types/service.ts b/src/types/service.ts index 20d0109a..31dcaaa8 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -5,7 +5,9 @@ import type { TRADE_TYPES, TRADE_STATUS, PRODUCT_CONDITIONS, - MESSAGE_SORT_OPTIONS + MESSAGE_SORT_OPTIONS, + VALID_NICKNAME_MESSAGE, + SCORE } from '@constants' /** 정렬 옵션 */ @@ -60,3 +62,11 @@ export type TradeBuyActivityNames = ValueOf /** 나의 거래 활동 - 리뷰 */ export type TradeReviewActivityCodes = KeyOf export type TradeReviewActivityNames = ValueOf + +/** 유효성 검사 메시지 */ +export type ValidNicknameMessages = ValueOf + +/** 리뷰 */ +export type Score = typeof SCORE +export type ScoreNames = KeyOf +export type ScoreCodes = ValueOf diff --git a/src/utils/index.ts b/src/utils/index.ts index ff2d2bdb..1c338e3d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './common' export * from './react' export * from './format' +export * from './valid' diff --git a/src/utils/valid/index.ts b/src/utils/valid/index.ts new file mode 100644 index 00000000..b71c36c2 --- /dev/null +++ b/src/utils/valid/index.ts @@ -0,0 +1,2 @@ +export const isNumber = (num: unknown): num is number => + !isNaN(num as number) && isFinite(num as number) && typeof num === 'number'