Skip to content

Commit

Permalink
Merge pull request #53 from uju-in/fix/reviewAndVotes-qa
Browse files Browse the repository at this point in the history
  • Loading branch information
yeeeerim authored Apr 3, 2024
2 parents 4efc2d8 + 4032117 commit 71bfdb9
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function ReviewImagesDisplay({
return (
<div
className={cn(
'flex w-[508px] gap-2 overflow-y-hidden overflow-x-scroll',
'flex w-[508px] gap-2 overflow-x-auto overflow-y-hidden',
'mo:w-[250px]',
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<boolean>>
Expand All @@ -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

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -108,7 +106,7 @@ export default function ReviewModal(props: PropsType) {

if (
action === 'edit' &&
!validateImage({ multipartReviewImages, existingImages, content })
!validateImage({ multipartReviewImages, existingImages })
) {
return
}
Expand Down Expand Up @@ -144,7 +142,7 @@ export default function ReviewModal(props: PropsType) {
return (
<Modal innerClassNames="mo:top-0 mo:max-w-full mo:max-h-full mo:-translate-y-0">
<form
className={cn('w-full p-[13px_0_45px]', 'mo:px-[16px]')}
className={cn('w-[590px] p-[13px_0_45px]', 'mo:w-full mo:px-[16px]')}
onSubmit={handleSubmit}
>
<div
Expand Down Expand Up @@ -234,14 +232,14 @@ export default function ReviewModal(props: PropsType) {
<textarea
name="content"
placeholder="최소 10자 이상 작성해주세요."
className="h-[152px] w-[508px] max-w-full resize-none border border-[#DADADA] bg-[#F4F4F4] p-[14px_12px] text-[14px] outline-none"
className="h-[152px] w-full resize-none border border-[#DADADA] bg-[#F4F4F4] p-[14px_12px] text-[14px] outline-none"
onChange={(e) => setContent(e.target.value)}
minLength={10}
maxLength={1000}
value={content}
required
/>
<div className="mo:flex mo:gap-[8px]">
<div className="w-full mo:flex mo:gap-[8px]">
<button
type="button"
onClick={() => {
Expand Down Expand Up @@ -285,6 +283,7 @@ export default function ReviewModal(props: PropsType) {
ref={InputRef}
className="hidden"
onChange={handleFileChange}
accept="image/jpeg, image/png, image/gif, image/bmp, image/svg+xml, image/tiff, image/webp"
/>
</div>
<div className="mt-[50px] flex w-full gap-[20px] text-[16px] font-semibold">
Expand Down
46 changes: 38 additions & 8 deletions src/app/(route)/(withLayout)/items/[itemId]/_utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import renderToast from '@/app/_utils/toast'

const MAX_CONTENT_LENGTH = 11
const MIN_COUNT = 0

/* 아이템 등록 - 이미지 별점 선택 검증 */
export const validateForm = ({
Expand All @@ -21,7 +22,7 @@ export const validateForm = ({
return false
}

if (rating === 0) {
if (rating === MIN_COUNT) {
renderToast({
type: 'error',
message: '별점을 선택해 주세요!',
Expand All @@ -30,7 +31,7 @@ export const validateForm = ({
return false
}

if (multipartReviewImages.length === 0) {
if (multipartReviewImages.length === MIN_COUNT) {
renderToast({
type: 'error',
message: '리뷰 사진을 첨부해 주세요.',
Expand All @@ -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: '이미지를 등록해 주세요.',
Expand All @@ -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
}
5 changes: 1 addition & 4 deletions src/app/(route)/(withLayout)/items/add-item/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import RQProvider from '@/app/_components/RQProvider'
import { cn } from '@/app/_utils/twMerge'
import PostForm from './_component/PostForm'

Expand All @@ -10,9 +9,7 @@ export default function AddItemPage() {
<div className="flex items-center mo:hidden">
<h1 className="text-[32px] font-bold">아이템 생성</h1>
</div>
<RQProvider>
<PostForm />
</RQProvider>
<PostForm />
</section>
)
}
2 changes: 2 additions & 0 deletions src/app/_hook/api/reviews/useAddReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function useAddReview() {
queryClient.invalidateQueries({
queryKey: reviewKeys.reviewList._def,
})
queryClient.invalidateQueries({ queryKey: itemKeys.itemDetail._def })
},
onError: (error) => {
renderToast({
Expand Down
15 changes: 4 additions & 11 deletions src/app/_hook/api/votes/useFavoritesList.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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,
})

Expand Down
2 changes: 1 addition & 1 deletion src/app/_hook/api/votes/useParticipationVote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const useParticipationVote = () => {
onSuccess: () => {
renderToast({
type: 'success',
message: '투표 취소 성공!',
message: '투표 성공!',
})

queryClient.invalidateQueries({
Expand Down
29 changes: 18 additions & 11 deletions src/app/_hook/api/votes/useVoteListData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/app/_utils/dateFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

0 comments on commit 71bfdb9

Please sign in to comment.