Skip to content

Commit

Permalink
[#15] feat: 리뷰 수정 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
reesekimm committed Oct 9, 2020
1 parent a9e16a6 commit 77d66b5
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 66 deletions.
2 changes: 1 addition & 1 deletion client/src/common/types/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions client/src/common/types/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +39,8 @@ export interface ReviewState {
mainReviews: Reviews;
addReviewDone: boolean;
addReviewError: string | null;
editReviewDone: boolean;
editReviewError: string | null;
deleteReviewDone: boolean;
deleteReviewError: string | null;
/** 이미 리뷰를 작성한 도서 클릭시 제공할 데이터 */
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/molecules/BookInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function BookInfo({
</Button>
</>
)}
{type !== 'write' && <Rate disabled defaultValue={rating} allowHalf />}
{type !== 'write' && <Rate disabled value={rating} allowHalf />}
</div>
</S.Container>
);
Expand Down
31 changes: 15 additions & 16 deletions client/src/components/molecules/ReviewActionBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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();
}, []);
Expand All @@ -80,14 +79,14 @@ function ReviewActionBar({
<S.ButtonContent>
<MessageOutlined key="comment" />
<Text color="gray4" fontSize="xsm">
댓글 {NumberOfComments}
댓글 {Comments.length}
</Text>
</S.ButtonContent>
</Button>
)}
{myId && User.id === myId && type === 'detail' ? (
<>
<Button styleType="plain">
<Button styleType="plain" onClick={onClickEditReview}>
<S.ButtonContent>
<EditOutlined />
<Text color="gray4" fontSize="xsm">
Expand Down Expand Up @@ -121,7 +120,7 @@ function ReviewActionBar({
<S.ButtonContent>
{liked ? <HeartFilled key="heart" /> : <HeartOutlined key="like" />}
<Text color="gray4" fontSize="xsm">
좋아요 {NumberOfLikes}
좋아요 {Likers.length}
</Text>
</S.ButtonContent>
</Button>
Expand Down
10 changes: 1 addition & 9 deletions client/src/components/molecules/ReviewListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,7 @@ function ReviewListItem(review: IReview.Review): React.ReactElement {
)}
</S.Content>
</S.ContentWrapper>
<ReviewActionBar
id={id}
User={User}
createdAt={createdAt}
NumberOfComments={Comments.length}
NumberOfLikes={Likers.length}
onClickComment={openReviewDetailModal}
type="list"
/>
<ReviewActionBar type="list" content={review} onClickComment={openReviewDetailModal} />
</S.Container>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,7 @@ function ReviewDetailTemplate({ content, closeModal }: Props): React.ReactElemen
<S.ReviewContainer>
<BookInfo {...bookInfo} />
<S.Content>{data}</S.Content>
<ReviewActionBar
id={id}
User={User}
createdAt={createdAt}
NumberOfComments={Comments.length}
NumberOfLikes={Likers.length}
type="detail"
/>
<ReviewActionBar type="detail" content={content} />
</S.ReviewContainer>
<S.CommentContainer>
<div>
Expand Down
29 changes: 18 additions & 11 deletions client/src/components/templates/WriteReviewTemplate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -42,17 +44,22 @@ function WriteReviewTemplate({ content, closeModal }: Props): React.ReactElement

/** 작성된 리뷰가 없을 경우 (리뷰 작성 OR 수정) */

const [rating, setRating] = useState<number>();
const [text, onChangeText] = useInput('');
const [rating, setRating] = useState<number>(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 (
<S.Container>
Expand All @@ -62,10 +69,10 @@ function WriteReviewTemplate({ content, closeModal }: Props): React.ReactElement
<ReviewForm
value={text}
onChange={onChangeText}
onSubmit={onSubmit}
submitButtonText="작성"
onSubmit={content.id ? onClickEditReview : onClickWriteReview}
submitButtonText={content.id ? '수정' : '작성'}
buttonDisabled={!rating || !text}
isLoading={addReview}
isLoading={content.id ? editReview : addReview}
style={{ flex: 1 }}
/>
) : (
Expand Down
2 changes: 1 addition & 1 deletion client/src/features/modal/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const modalSlice = createSlice({
name: 'modal',
initialState,
reducers: {
openWriteReviewModal: (state, action: PayloadAction<IBook.Book>) => {
openWriteReviewModal: (state, action: PayloadAction<IBook.Book | IReview.ReviewInfo>) => {
state.writeReviewModal.isOpened = true;
state.writeReviewModal.data = action.payload;
},
Expand Down
49 changes: 33 additions & 16 deletions client/src/features/review/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const initialState: IReview.ReviewState = {
mainReviews: [],
addReviewDone: false,
addReviewError: null,
editReviewDone: false,
editReviewError: null,
deleteReviewDone: false,
deleteReviewError: null,
Review: null,
Expand All @@ -33,22 +35,6 @@ const reviewSlice = createSlice({
name: 'review',
initialState,
reducers: {
getReview: (state, action: PayloadAction<IBook.ISBN>) => {
state.Review = null;
state.getReviewDone = false;
state.getReviewError = null;
},
getReviewSuccess: (state, action: PayloadAction<IBook.ISBN>) => {
state.getReviewDone = true;
state.Review = reviews.find((review) => review.Book.isbn13 === '9788966262595');
},
getReviewFailure: (state, action: PayloadAction<string>) => {
state.getReviewDone = true;
state.getReviewError = action.payload;
},
clearReview: (state) => {
state.Review = null;
},
addReview: (state, actions) => {
state.addReviewDone = false;
state.addReviewError = null;
Expand All @@ -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<IReview.ReviewId>) => {
state.deleteReviewDone = false;
state.deleteReviewError = null;
Expand All @@ -73,6 +74,22 @@ const reviewSlice = createSlice({
deleteReviewFailure: (state, action) => {
state.deleteReviewError = action.payload;
},
getReview: (state, action: PayloadAction<IBook.ISBN>) => {
state.Review = null;
state.getReviewDone = false;
state.getReviewError = null;
},
getReviewSuccess: (state, action: PayloadAction<IBook.ISBN>) => {
state.getReviewDone = true;
state.Review = reviews.find((review) => review.Book.isbn13 === '9788966262595');
},
getReviewFailure: (state, action: PayloadAction<string>) => {
state.getReviewDone = true;
state.getReviewError = action.payload;
},
clearReview: (state) => {
state.Review = null;
},
},
});

Expand Down
10 changes: 7 additions & 3 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);

Expand Down
17 changes: 17 additions & 0 deletions client/src/sagas/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand All @@ -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`;
Expand All @@ -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());
Expand All @@ -74,6 +90,7 @@ function* watchClearReview() {
export default function* reviewSaga(): Generator {
yield all([
fork(watchAddReview),
fork(watchEditReview),
fork(watchDeleteReview),
fork(watchGetReview),
fork(watchClearReview),
Expand Down

0 comments on commit 77d66b5

Please sign in to comment.