diff --git a/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewImagesDisplay.tsx b/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewImagesDisplay.tsx index 8ff4a248..b7f48c97 100644 --- a/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewImagesDisplay.tsx +++ b/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewImagesDisplay.tsx @@ -32,7 +32,7 @@ export default function ReviewImagesDisplay({ return (
diff --git a/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewModal.tsx b/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewModal.tsx index b6607fcb..e387dd08 100644 --- a/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewModal.tsx +++ b/src/app/(route)/(withLayout)/items/[itemId]/_component/ReviewModal.tsx @@ -1,20 +1,19 @@ 'use client' -import React, { ChangeEvent, useRef, useState } from 'react' +import { ChangeEvent, useRef, useState } from 'react' import Image from 'next/image' - import { ReviewInfo } from '@/app/_types/review.type' - import useAddReview from '@/app/_hook/api/reviews/useAddReview' import useEditReview from '@/app/_hook/api/reviews/useEditReview' - import Modal from '@/app/_components/modal' import { cn } from '@/app/_utils/twMerge' -import renderToast from '@/app/_utils/toast' import StarRatingFormatter from './StarRatingFormatter' import ReviewImagesDisplay from './ReviewImagesDisplay' - -import { validateForm, validateImage } from '../_utils/validation' +import { + validateForm, + validateImage, + validateImageSize, +} from '../_utils/validation' interface PropsType { setShowReviewModal: React.Dispatch> @@ -28,8 +27,6 @@ interface PropsType { review?: ReviewInfo } -const MAX_IMAGE_COUNT = 5 - export default function ReviewModal(props: PropsType) { const { setShowReviewModal, itemData, action, review } = props @@ -59,12 +56,13 @@ export default function ReviewModal(props: PropsType) { if (e.target.files) { const filesArray = Array.from(e.target.files) - const totalImagesCount = - existingImages.length + multipartReviewImages.length + filesArray.length - - if (totalImagesCount > MAX_IMAGE_COUNT) { - renderToast({ type: 'error', message: '최대 5장까지 등록 가능합니다.' }) + if (!validateImageSize(filesArray)) { + return + } + if ( + !validateImage({ existingImages, multipartReviewImages, filesArray }) + ) { return } @@ -108,7 +106,7 @@ export default function ReviewModal(props: PropsType) { if ( action === 'edit' && - !validateImage({ multipartReviewImages, existingImages, content }) + !validateImage({ multipartReviewImages, existingImages }) ) { return } @@ -144,7 +142,7 @@ export default function ReviewModal(props: PropsType) { return (
setContent(e.target.value)} minLength={10} maxLength={1000} value={content} required /> -
+
diff --git a/src/app/(route)/(withLayout)/items/[itemId]/_utils/validation.ts b/src/app/(route)/(withLayout)/items/[itemId]/_utils/validation.ts index bad713ee..60135022 100644 --- a/src/app/(route)/(withLayout)/items/[itemId]/_utils/validation.ts +++ b/src/app/(route)/(withLayout)/items/[itemId]/_utils/validation.ts @@ -1,6 +1,7 @@ import renderToast from '@/app/_utils/toast' const MAX_CONTENT_LENGTH = 11 +const MIN_COUNT = 0 /* 아이템 등록 - 이미지 별점 선택 검증 */ export const validateForm = ({ @@ -21,7 +22,7 @@ export const validateForm = ({ return false } - if (rating === 0) { + if (rating === MIN_COUNT) { renderToast({ type: 'error', message: '별점을 선택해 주세요!', @@ -30,7 +31,7 @@ export const validateForm = ({ return false } - if (multipartReviewImages.length === 0) { + if (multipartReviewImages.length === MIN_COUNT) { renderToast({ type: 'error', message: '리뷰 사진을 첨부해 주세요.', @@ -42,26 +43,35 @@ export const validateForm = ({ return true } -/* 이미지 갯수 검증 */ +/* 이미지 개수 검증 */ export const validateImage = ({ existingImages, multipartReviewImages, - content, + filesArray, }: { existingImages: string[] multipartReviewImages: File[] - content: string + filesArray?: File[] }) => { - if (content.trim().length < MAX_CONTENT_LENGTH) { + const MAX_IMAGE_COUNT = 5 + + const files = filesArray || [] + + const totalImagesCount = + existingImages.length + multipartReviewImages.length + files.length + + /** 최대 이미지 개수 */ + if (totalImagesCount > MAX_IMAGE_COUNT) { renderToast({ type: 'error', - message: '리뷰 내용을 최소 10자 이상 작성해 주세요..', + message: '최대 5장까지 등록 가능합니다.', }) return false } - if (existingImages.length + multipartReviewImages.length === 0) { + /** 최소 이미지 개수 */ + if (totalImagesCount < MIN_COUNT) { renderToast({ type: 'error', message: '이미지를 등록해 주세요.', @@ -72,3 +82,23 @@ export const validateImage = ({ return true } + +/** 이미지 용량 */ +export const validateImageSize = (filesArray: File[]) => { + const MAX_IMAGE_SIZE = 5 * 1024 * 1024 + + const isExceedingSize = filesArray.some( + (file: File) => file.size > MAX_IMAGE_SIZE, + ) + + if (isExceedingSize) { + renderToast({ + type: 'error', + message: '이미지 파일 크기는 5MB를 초과할 수 없습니다.', + }) + + return false + } + + return true +} diff --git a/src/app/(route)/(withLayout)/items/add-item/page.tsx b/src/app/(route)/(withLayout)/items/add-item/page.tsx index ecddcf65..5edca148 100644 --- a/src/app/(route)/(withLayout)/items/add-item/page.tsx +++ b/src/app/(route)/(withLayout)/items/add-item/page.tsx @@ -1,4 +1,3 @@ -import RQProvider from '@/app/_components/RQProvider' import { cn } from '@/app/_utils/twMerge' import PostForm from './_component/PostForm' @@ -10,9 +9,7 @@ export default function AddItemPage() {

아이템 생성

- - - + ) } diff --git a/src/app/_hook/api/reviews/useAddReview.ts b/src/app/_hook/api/reviews/useAddReview.ts index aa9bf92e..ae58f947 100644 --- a/src/app/_hook/api/reviews/useAddReview.ts +++ b/src/app/_hook/api/reviews/useAddReview.ts @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import renderToast from '@/app/_utils/toast' import { getCookie } from 'cookies-next' import { reviewKeys } from '.' +import { itemKeys } from '../items' interface AddReviewRequest { itemId: number @@ -43,6 +44,7 @@ export default function useAddReview() { queryClient.invalidateQueries({ queryKey: reviewKeys.reviewList._def, }) + queryClient.invalidateQueries({ queryKey: itemKeys.itemDetail._def }) }, onError: (error) => { renderToast({ diff --git a/src/app/_hook/api/votes/useFavoritesList.ts b/src/app/_hook/api/votes/useFavoritesList.ts index bd268831..a3b66a84 100644 --- a/src/app/_hook/api/votes/useFavoritesList.ts +++ b/src/app/_hook/api/votes/useFavoritesList.ts @@ -1,18 +1,14 @@ import { useQuery } from '@tanstack/react-query' -import { useCookies } from 'next-client-cookies' +import { getCookie } from 'cookies-next' import { voteKeys } from '.' interface RequestInfo { type: 'folder' | 'item' folderId?: number | null - accessToken: string } -export async function fetchFavoriteList({ - type, - folderId, - accessToken, -}: RequestInfo) { +export async function fetchFavoriteList({ type, folderId }: RequestInfo) { + const accessToken = getCookie('accessToken') let url = `${process.env.NEXT_PUBLIC_BASE_URL}/api/favorites?favoriteTypeCondition=${type}` if (type === 'item' && folderId) { @@ -40,16 +36,13 @@ export const useFavoritesList = ( type: 'folder' | 'item', folderId?: number | null, ) => { - const cookies = useCookies() - const accessToken = cookies.get('accessToken') ?? '' - const { data: itemList, isError, isSuccess, } = useQuery({ queryKey: voteKeys.favorites(folderId as number).queryKey, - queryFn: () => fetchFavoriteList({ type, folderId, accessToken }), + queryFn: () => fetchFavoriteList({ type, folderId }), staleTime: 1000 * 60, }) diff --git a/src/app/_hook/api/votes/useParticipationVote.ts b/src/app/_hook/api/votes/useParticipationVote.ts index 7a054f8e..18fd8491 100644 --- a/src/app/_hook/api/votes/useParticipationVote.ts +++ b/src/app/_hook/api/votes/useParticipationVote.ts @@ -41,7 +41,7 @@ export const useParticipationVote = () => { onSuccess: () => { renderToast({ type: 'success', - message: '투표 취소 성공!', + message: '투표 성공!', }) queryClient.invalidateQueries({ diff --git a/src/app/_hook/api/votes/useVoteListData.ts b/src/app/_hook/api/votes/useVoteListData.ts index fb168325..99066afe 100644 --- a/src/app/_hook/api/votes/useVoteListData.ts +++ b/src/app/_hook/api/votes/useVoteListData.ts @@ -9,23 +9,26 @@ interface VoteQueryParams { sortOption: string } +const VOTE_FETCH_SIZE = 6 + async function fetchVoteData({ pageParam, hobby, sortOption, }: VoteQueryParams) { - const SIZE = 6 - - const res = await fetch( - `${process.env.NEXT_PUBLIC_BASE_URL}/api/votes?hobby=${hobby}&cursorId=${pageParam}&size=${SIZE}&sort=${sortOption}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - cache: 'no-store', + let URL = `${process.env.NEXT_PUBLIC_BASE_URL}/api/votes?hobby=${hobby}&size=${VOTE_FETCH_SIZE}&sort=${sortOption}` + + if (pageParam) { + URL += `&cursorId=${pageParam}` + } + + const res = await fetch(URL, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - ) + cache: 'no-store', + }) const data = await res.json() @@ -44,6 +47,10 @@ export const useVoteListData = (hobby: string, sortOption: string) => { fetchVoteData({ pageParam, sortOption, hobby }), initialPageParam: null, getNextPageParam: (lastPage: PagesResponse) => { + if (lastPage.totalCount < VOTE_FETCH_SIZE) { + return null + } + return lastPage.nextCursorId }, staleTime: 1000 * 60, diff --git a/src/app/_utils/dateFormatter.ts b/src/app/_utils/dateFormatter.ts index 4ad771c4..f142948b 100644 --- a/src/app/_utils/dateFormatter.ts +++ b/src/app/_utils/dateFormatter.ts @@ -27,6 +27,6 @@ export function detailDateFormatter(dateString: string): string { const formattedDate = new Intl.DateTimeFormat('ko-KR', options).format(date) return formattedDate - .replace(/(\d{4})\. (\d{2})\. (\d{2})\. (\d{2}):(\d{2})/, '$1.$2.$3.$4.$5') + .replace(/(\d{4})\. (\d{2})\. (\d{2})\. (\d{2}):(\d{2})/, '$1.$2.$3 $4:$5') .trim() }