From 77d66b50feeed2b9cd9f772838f44184ce9a2cb6 Mon Sep 17 00:00:00 2001 From: Reese Kim Date: Fri, 9 Oct 2020 17:20:31 +0900 Subject: [PATCH] =?UTF-8?q?[#15]=20feat:=20=EB=A6=AC=EB=B7=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/common/types/modal.ts | 2 +- client/src/common/types/review.ts | 9 ++++ .../components/molecules/BookInfo/index.tsx | 2 +- .../molecules/ReviewActionBar/index.tsx | 31 ++++++------ .../molecules/ReviewListItem/index.tsx | 10 +--- .../templates/ReviewDetailTemplate/index.tsx | 9 +--- .../templates/WriteReviewTemplate/index.tsx | 29 ++++++----- client/src/features/modal/slice.ts | 2 +- client/src/features/review/slice.ts | 49 +++++++++++++------ client/src/lib/api.ts | 10 ++-- client/src/sagas/review.ts | 17 +++++++ 11 files changed, 104 insertions(+), 66 deletions(-) diff --git a/client/src/common/types/modal.ts b/client/src/common/types/modal.ts index 231fa5b..10f3eba 100644 --- a/client/src/common/types/modal.ts +++ b/client/src/common/types/modal.ts @@ -3,7 +3,7 @@ import { IBook, IReview } from '@types'; export interface ModalState { writeReviewModal: { isOpened: boolean; - data: IBook.Book | null; + data: IBook.Book | IReview.ReviewInfo | null; }; reviewDetailModal: { isOpened: boolean; diff --git a/client/src/common/types/review.ts b/client/src/common/types/review.ts index dae99e2..1f1cba4 100644 --- a/client/src/common/types/review.ts +++ b/client/src/common/types/review.ts @@ -15,6 +15,13 @@ export interface Liker { id: number | string; // TODO: DB 연동후 number로 고정 } +export interface ReviewInfo { + id: number | string; + Book: IBook.Book; + rating: number; + content: string; +} + export interface Review { id: ReviewId; User: IUser.User; @@ -32,6 +39,8 @@ export interface ReviewState { mainReviews: Reviews; addReviewDone: boolean; addReviewError: string | null; + editReviewDone: boolean; + editReviewError: string | null; deleteReviewDone: boolean; deleteReviewError: string | null; /** 이미 리뷰를 작성한 도서 클릭시 제공할 데이터 */ diff --git a/client/src/components/molecules/BookInfo/index.tsx b/client/src/components/molecules/BookInfo/index.tsx index e493c98..39e29b1 100644 --- a/client/src/components/molecules/BookInfo/index.tsx +++ b/client/src/components/molecules/BookInfo/index.tsx @@ -47,7 +47,7 @@ function BookInfo({ )} - {type !== 'write' && } + {type !== 'write' && } ); diff --git a/client/src/components/molecules/ReviewActionBar/index.tsx b/client/src/components/molecules/ReviewActionBar/index.tsx index 09eba0d..c4ea5f6 100644 --- a/client/src/components/molecules/ReviewActionBar/index.tsx +++ b/client/src/components/molecules/ReviewActionBar/index.tsx @@ -14,7 +14,7 @@ import 'dayjs/locale/ko'; import { RootState } from '@features'; import { Text, Button, Modal, FeedbackTemplate } from '@components'; import { useModal } from '@hooks'; -import { IReview, IUser } from '@types'; +import { IReview } from '@types'; import * as S from './style'; import { actions } from '../../../features/review'; import { actions as modalActions } from '../../../features/modal'; @@ -23,26 +23,20 @@ dayjs.extend(relativeTime); dayjs.locale('ko'); interface Props { - id: IReview.ReviewId; - User: IUser.User; - createdAt: IReview.createdAt; - NumberOfComments: number; - NumberOfLikes: number; - onClickComment?: () => void; /** Action Bar 타입 (list(default) : 리뷰 목록, detail : 리뷰 상세) */ type: string; + content: IReview.Review; + onClickComment?: () => void; } function ReviewActionBar({ - id, - User, - createdAt, - NumberOfComments, - NumberOfLikes, - onClickComment, type = 'list', + content, + onClickComment, ...props }: Props): React.ReactElement { + const { id, User, Book, rating, content: data, createdAt, Comments, Likers } = content; + const dispatch = useDispatch(); const myId = useSelector((state: RootState) => state.user.me?.id); const { deleteReview } = useSelector((state: RootState) => state.loading); @@ -55,6 +49,11 @@ function ReviewActionBar({ setLiked((prev) => !prev); }, []); + const onClickEditReview = useCallback(() => { + dispatch(modalActions.closeReviewDetailModal()); + dispatch(modalActions.openWriteReviewModal({ id, Book, rating, content: data })); + }, []); + const onClickDeleteReview = useCallback(() => { toggleFeedbackModal(); }, []); @@ -80,14 +79,14 @@ function ReviewActionBar({ - 댓글 {NumberOfComments} + 댓글 {Comments.length} )} {myId && User.id === myId && type === 'detail' ? ( <> - diff --git a/client/src/components/molecules/ReviewListItem/index.tsx b/client/src/components/molecules/ReviewListItem/index.tsx index aff0d18..64c68a6 100644 --- a/client/src/components/molecules/ReviewListItem/index.tsx +++ b/client/src/components/molecules/ReviewListItem/index.tsx @@ -33,15 +33,7 @@ function ReviewListItem(review: IReview.Review): React.ReactElement { )} - + ); } diff --git a/client/src/components/templates/ReviewDetailTemplate/index.tsx b/client/src/components/templates/ReviewDetailTemplate/index.tsx index 8673667..b081a30 100644 --- a/client/src/components/templates/ReviewDetailTemplate/index.tsx +++ b/client/src/components/templates/ReviewDetailTemplate/index.tsx @@ -18,14 +18,7 @@ function ReviewDetailTemplate({ content, closeModal }: Props): React.ReactElemen {data} - +
diff --git a/client/src/components/templates/WriteReviewTemplate/index.tsx b/client/src/components/templates/WriteReviewTemplate/index.tsx index 9fecfc4..2582a7a 100644 --- a/client/src/components/templates/WriteReviewTemplate/index.tsx +++ b/client/src/components/templates/WriteReviewTemplate/index.tsx @@ -6,25 +6,27 @@ import { GithubOutlined } from '@ant-design/icons'; import { RootState } from '@features'; import { Button, Text, BookInfo, ReviewForm, Modal, FeedbackTemplate } from '@components'; import { useModal, useInput } from '@hooks'; -import { IBook, IUser } from '@types'; +import { IBook, IUser, IReview } from '@types'; import { actions } from '../../../features/review'; import { actions as modalActions } from '../../../features/modal'; import * as S from './style'; interface Props { - content: IBook.Book; + content: IBook.Book | IReview.ReviewInfo; /** template을 감싸고 있는 Modal을 닫는 callback */ closeModal: () => void; } function WriteReviewTemplate({ content, closeModal }: Props): React.ReactElement { - const bookInfo = { ...content, type: 'write' } as const; + const bookInfo = content.isbn + ? { ...content, type: 'write' } + : { ...content.Book, type: 'write' }; const { modalIsOpened: feedbackModalIsOpened, toggleModal: toggleFeedbackModal } = useModal(); const dispatch = useDispatch(); const { me } = useSelector((state: RootState) => state.user); - const { addReview } = useSelector((state: RootState) => state.loading); + const { addReview, editReview } = useSelector((state: RootState) => state.loading); /** 작성된 리뷰가 있는지 확인 후 피드백 */ useEffect(() => { @@ -42,17 +44,22 @@ function WriteReviewTemplate({ content, closeModal }: Props): React.ReactElement /** 작성된 리뷰가 없을 경우 (리뷰 작성 OR 수정) */ - const [rating, setRating] = useState(); - const [text, onChangeText] = useInput(''); + const [rating, setRating] = useState(content.rating || 0); + const [text, onChangeText] = useInput(content.content || ''); const onChangeRate = useCallback((value) => { setRating(value); }, []); - const onSubmit = useCallback(() => { + const onClickWriteReview = useCallback(() => { dispatch(actions.addReview({ Book: content, rating, content: text })); closeModal(); - }, [text, rating]); + }, [rating, text]); + + const onClickEditReview = useCallback(() => { + dispatch(actions.editReview({ id: content.id, rating, content: text })); + closeModal(); + }, [rating, text]); return ( @@ -62,10 +69,10 @@ function WriteReviewTemplate({ content, closeModal }: Props): React.ReactElement ) : ( diff --git a/client/src/features/modal/slice.ts b/client/src/features/modal/slice.ts index 7579bf7..5adaea4 100644 --- a/client/src/features/modal/slice.ts +++ b/client/src/features/modal/slice.ts @@ -17,7 +17,7 @@ const modalSlice = createSlice({ name: 'modal', initialState, reducers: { - openWriteReviewModal: (state, action: PayloadAction) => { + openWriteReviewModal: (state, action: PayloadAction) => { state.writeReviewModal.isOpened = true; state.writeReviewModal.data = action.payload; }, diff --git a/client/src/features/review/slice.ts b/client/src/features/review/slice.ts index e88528d..21b6a35 100644 --- a/client/src/features/review/slice.ts +++ b/client/src/features/review/slice.ts @@ -8,6 +8,8 @@ export const initialState: IReview.ReviewState = { mainReviews: [], addReviewDone: false, addReviewError: null, + editReviewDone: false, + editReviewError: null, deleteReviewDone: false, deleteReviewError: null, Review: null, @@ -33,22 +35,6 @@ const reviewSlice = createSlice({ name: 'review', initialState, reducers: { - getReview: (state, action: PayloadAction) => { - state.Review = null; - state.getReviewDone = false; - state.getReviewError = null; - }, - getReviewSuccess: (state, action: PayloadAction) => { - state.getReviewDone = true; - state.Review = reviews.find((review) => review.Book.isbn13 === '9788966262595'); - }, - getReviewFailure: (state, action: PayloadAction) => { - state.getReviewDone = true; - state.getReviewError = action.payload; - }, - clearReview: (state) => { - state.Review = null; - }, addReview: (state, actions) => { state.addReviewDone = false; state.addReviewError = null; @@ -61,6 +47,21 @@ const reviewSlice = createSlice({ state.addReviewDone = true; state.addReviewError = action.payload; }, + editReview: (state, action) => { + state.editReviewDone = false; + state.editReviewError = null; + }, + editReviewSuccess: (state, action) => { + const reviewIndex = state.mainReviews.findIndex((review) => review.id === action.payload.id); + state.mainReviews[reviewIndex].rating = action.payload.rating; + state.mainReviews[reviewIndex].content = action.payload.content; + state.editReviewDone = true; + state.editReviewError = null; + }, + editReviewFailure: (state, action) => { + state.editReviewDone = true; + state.editReviewError = action.payload; + }, deleteReview: (state, action: PayloadAction) => { state.deleteReviewDone = false; state.deleteReviewError = null; @@ -73,6 +74,22 @@ const reviewSlice = createSlice({ deleteReviewFailure: (state, action) => { state.deleteReviewError = action.payload; }, + getReview: (state, action: PayloadAction) => { + state.Review = null; + state.getReviewDone = false; + state.getReviewError = null; + }, + getReviewSuccess: (state, action: PayloadAction) => { + state.getReviewDone = true; + state.Review = reviews.find((review) => review.Book.isbn13 === '9788966262595'); + }, + getReviewFailure: (state, action: PayloadAction) => { + state.getReviewDone = true; + state.getReviewError = action.payload; + }, + clearReview: (state) => { + state.Review = null; + }, }, }); diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index fe043da..9c8612b 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -6,9 +6,13 @@ const BASE_URL = process.env.NEXT_PUBLIC_DEV_API; export const login = (data) => axios.post('/api/login', data); -export const addReview = (data) => axios.post(`${BASE_URL}/review`, data); -export const deleteReview = (reviewId: IReview.ReviewId) => - axios.delete(`${BASE_URL}/review/${reviewId}`); +export const addReview = (review) => axios.post(`${BASE_URL}/review`, review); + +export const editReview = ({ id, rating, content }) => + axios.patch(`${BASE_URL}/review/${id}`, { rating, content }); + +export const deleteReview = (id: IReview.ReviewId) => axios.delete(`${BASE_URL}/review/${id}`); + export const getReview = ({ id, isbn13 }) => axios.get(`${BASE_URL}/review?id=${id}&isbn13=${isbn13}`); diff --git a/client/src/sagas/review.ts b/client/src/sagas/review.ts index 7b51706..74ca26e 100644 --- a/client/src/sagas/review.ts +++ b/client/src/sagas/review.ts @@ -6,6 +6,8 @@ import { actions } from '../features/review'; import { actions as userActions } from '../features/user'; import { actions as loadingActions } from '../features/loading'; +/** 리뷰 작성 */ + function* addReview({ type, payload }) { const success = `${type}Success`; const failure = `${type}Failure`; @@ -32,6 +34,16 @@ function* watchAddReview() { yield takeLatest(actions.addReview, addReview); } +/** 리뷰 수정 */ + +const editReview = createRequestSaga(actions.editReview, `api.editReview`); + +function* watchEditReview() { + yield takeLatest(actions.editReview, editReview); +} + +/** 리뷰 삭제 */ + function* deleteReview({ type, payload }) { const success = `${type}Success`; const failure = `${type}Failure`; @@ -58,12 +70,16 @@ function* watchDeleteReview() { yield takeLatest(actions.deleteReview, deleteReview); } +/** 내가 작성한 리뷰 가져오기 */ + const getReview = createRequestSaga(actions.getReview, `api.getReview`); function* watchGetReview() { yield takeLatest(actions.getReview, getReview); } +/** 내가 작성한 리뷰 삭제 */ + function* watchClearReview() { while (true) { const action = yield take(actions.clearReview.toString()); @@ -74,6 +90,7 @@ function* watchClearReview() { export default function* reviewSaga(): Generator { yield all([ fork(watchAddReview), + fork(watchEditReview), fork(watchDeleteReview), fork(watchGetReview), fork(watchClearReview),