Skip to content

Commit

Permalink
feat: userData 저장, 댓글 좋아요 optimistic update 추가 (#18)
Browse files Browse the repository at this point in the history
* chore: 불필요한 console.log 삭제

* feat: logout api 추가

* feat: toastify 설정 추가

* feat: 사용자 정보 저장 및 로그아웃 추가

* feat: 댓글 작성자와 사용자가 같을 시 삭제 버튼

* feat: header 메뉴 중 로그인이 필요할 시 로그인페이지로 이동

* feat: 댓글 좋아요 Optimistic update 기능 추가
  • Loading branch information
Hellol77 authored Sep 29, 2024
1 parent 1e69d20 commit 96647fd
Show file tree
Hide file tree
Showing 27 changed files with 4,822 additions and 2,834 deletions.
23 changes: 23 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
Post,
Req,
Res,
UseGuards,
Expand Down Expand Up @@ -34,4 +36,25 @@ export class AuthController {

return res.redirect(process.env.REDIRECT_URI);
}

@Post('logout')
@ApiOperation({ summary: '로그아웃' })
async logout(@Res() res: Response) {
try {
// 로그아웃 처리 (쿠키 삭제)
res.clearCookie('accessToken', {
httpOnly: true,
secure: true,
sameSite: 'strict',
});

// 성공 메시지 반환
return res.status(HttpStatus.OK).json({ message: '로그아웃되었습니다.' });
} catch (error) {
// 예외 발생 시 처리
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
message: '로그아웃에 실패했습니다. 나중에 다시 시도해주세요.',
});
}
}
}
4 changes: 2 additions & 2 deletions backend/src/comment/comment.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import { User } from 'src/user/user.entity';

@Entity()
export class Comment {
@PrimaryGeneratedColumn()
@PrimaryGeneratedColumn('uuid')
@ApiProperty({
description: '댓글의 고유 식별자',
example: '1',
example: '1212313',
})
comment_id: string;

Expand Down
2 changes: 2 additions & 0 deletions backend/src/comment/comment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class CommentService {
user: comment.user,
likeCount: comment.likeCount,
isLikedByUser: false,
item_id: item_id,
};

if (kakao_id) {
Expand Down Expand Up @@ -106,6 +107,7 @@ export class CommentService {
user: comment.user,
likeCount: comment.likeCount,
isLikedByUser: false,
item_id,
};

if (kakao_id) {
Expand Down
6 changes: 6 additions & 0 deletions backend/src/comment/dto/comment.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export class CommentDto {
@ApiProperty({ description: 'The ID of the comment' })
comment_id: string;

@IsNotEmpty()
@ApiProperty({
description: 'The ID of the item to which the comment belongs',
})
item_id: string;

@IsNotEmpty()
@ApiProperty({ description: 'The text of the comment' })
comment_text: string;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/game/dto/game.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export class GameDto {
created_at: Date;

@IsNotEmpty()
@ApiProperty({ description: 'The user who made the game', type: Number })
@ApiProperty({ description: 'The user who made the game' })
user_id: string;
}
21 changes: 17 additions & 4 deletions backend/src/item/item.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ export class ItemController {
page,
limit,
);

const commentsWithItemId = comments.map((comment) => ({
...comment,
item_id,
}));
const total = await this.commentService.countComments(item_id);
return { data: comments, total };
return { data: commentsWithItemId, total };
}

@Get(':item_id/comments/best')
Expand All @@ -62,7 +67,6 @@ export class ItemController {
@Param('item_id') item_id: string,
): Promise<CommentDto[]> {
const token = req.cookies['accessToken']; // 쿠키에서 accessToken 읽기
console.log('token:', token);
let userId: string | null = null;
if (token) {
try {
Expand All @@ -75,7 +79,16 @@ export class ItemController {
console.log('유효하지 않은 토큰입니다.', error.message);
}
}
console.log('userId:', userId);
return this.commentService.findBestCommentsByItemId(item_id, userId);
const bestComments = await this.commentService.findBestCommentsByItemId(
item_id,
userId,
);

const bestCommentsWithItemId = bestComments.map((comment) => ({
...comment,
item_id,
}));

return bestCommentsWithItemId;
}
}
4 changes: 2 additions & 2 deletions backend/src/like/like.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
@Entity()
@Unique(['comment', 'user'])
export class Like {
@PrimaryGeneratedColumn()
@ApiProperty({ description: '좋아요의 id', example: 1 })
@PrimaryGeneratedColumn('uuid')
@ApiProperty({ description: '좋아요의 id', example: '1' })
like_id: string;

@CreateDateColumn()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/user/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IsNotEmpty } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty()
user_id: number;
user_id: string;

@IsNotEmpty()
username: string;
Expand Down
Binary file added frontend/.yarn/install-state.gz
Binary file not shown.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.1",
"react-toastify": "^10.0.5",
"styled-components": "^6.1.13"
},
"devDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/apis/deleteLike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { axiosInstance } from '.';
import END_POINTS from '../constants/api';

export default async function deleteLike(commentId) {
const { data } = await axiosInstance.delete(END_POINTS.DELETE_LIKE(commentId));

return data;
}
6 changes: 6 additions & 0 deletions frontend/src/apis/getUserInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { axiosInstance } from '.';

export default async function getUserInfo() {
const { data } = await axiosInstance.get('/user');
return data;
}
8 changes: 8 additions & 0 deletions frontend/src/apis/postLike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { axiosInstance } from '.';
import END_POINTS from '../constants/api';

export default async function postLike(commentId) {
const { data } = await axiosInstance.post(END_POINTS.POST_LIKE(commentId));

return data;
}
7 changes: 7 additions & 0 deletions frontend/src/apis/postLogout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { axiosInstance } from '.';
import END_POINTS from '../constants/api';

export default async function postLogout() {
const { data } = await axiosInstance.post(END_POINTS.POST_LOGOUT);
return data;
}
14 changes: 9 additions & 5 deletions frontend/src/components/comments/Comments.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useEffect, useRef } from 'react';
import { useContext, useEffect, useRef } from 'react';
import Comment from '../comments/comment/Comment';
import useGetBestComments from '../../hooks/queries/useGetBestComments';
import useGetComments from '../../hooks/queries/useGetComments';
import useIntersectionObserver from '../../hooks/useIntersectionObserver';
import * as S from './Comments.styled';
import { UserInfoContext } from '../../context/UserInfoContext';
export default function Comments(props) {
const { bestButtonColor, itemId } = props;
const { userInfo } = useContext(UserInfoContext);
const { data: bestComments } = useGetBestComments(itemId);
const {
data: commentsPageData,
Expand All @@ -27,29 +29,31 @@ export default function Comments(props) {
<Comment
key={comment.comment_id}
isBest={true}
isTrash={true}
isTrash={userInfo?.user_id === comment.user.user_id ? true : false}
isHeart={comment.isLikedByUser}
bestButtonColor={bestButtonColor}
id={comment.comment_id}
commentId={comment.comment_id}
nickname={comment.user.username}
time={comment.created_at}
like={comment.likeCount}
comment={comment.comment_text}
itemId={comment.item_id}
/>
))}
{commentsPageData?.pages?.map((page) =>
page.data.map((comment) => (
<Comment
key={comment.comment_id}
isBest={false}
isTrash={false}
isTrash={userInfo?.user_id === comment.user.user_id ? true : false}
isHeart={comment.isLikedByUser}
bestButtonColor={bestButtonColor}
id={comment.comment_id}
commentId={comment.comment_id}
nickname={comment.user.username}
time={comment.created_at}
like={comment.likeCount}
comment={comment.comment_text}
itemId={comment.item_id}
/>
)),
)}
Expand Down
27 changes: 23 additions & 4 deletions frontend/src/components/comments/comment/Comment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ import * as S from './Comment.styled';
import TrashIconImg from '../../common/icon/TrashIcon';
import HeartIconImg from '../../common/icon/HeartIcon';
import timeAgo from '../../../utils/timeAgo';
import { useState } from 'react';
import useBestCommentLike from '../../../hooks/queries/useBestCommentLike';
import useCommentLike from '../../../hooks/queries/useCommentLike';

export default function Comment(props) {
const [isHeart, setIsHeart] = useState(props.isHeart);
const { mutate: bestCommentLikeMutate } = useBestCommentLike(props.commentId, props.itemId);
const { mutate: commentLikeMutate } = useCommentLike(props.commentId, props.itemId);
const handleClick = () => {
setIsHeart((prev) => !prev); //하트 컬러 변경 함수 호출
console.log('click');
if (props.isBest) {
bestCommentLikeMutate({ isHeart: props.isHeart });
console.log('best');
return;
} else {
commentLikeMutate({ isHeart: props.isHeart });
console.log('not best');
return;
}
};

// const handleLike = () => {
// if (isHeart) {
// //좋아요 취소
// return;
// }
// //좋아요
// };

return (
<S.CommentContainer isBest={props.isBest}>
<S.TopContainer key={props.id}>
Expand All @@ -22,7 +41,7 @@ export default function Comment(props) {
<TrashIconImg />
</S.TrashIcon>
<S.LikeContainer>
<S.HeartIcon onClick={handleClick} isHeart={isHeart}>
<S.HeartIcon onClick={handleClick} isHeart={props.isHeart}>
<HeartIconImg />
</S.HeartIcon>
<S.LikeStyle>{props.like}</S.LikeStyle>
Expand Down
29 changes: 24 additions & 5 deletions frontend/src/components/header/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import * as S from './Header.styled';
import { Link } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useContext } from 'react';
import { UserInfoContext } from '../../context/UserInfoContext';

export default function Header() {
const location = useLocation();
const { userInfo, logout } = useContext(UserInfoContext);

const handleLogout = () => {
logout();
};

return (
<S.HeaderContainer>
Expand All @@ -16,13 +23,25 @@ export default function Header() {
<S.HeaderTitle>SISO</S.HeaderTitle>
</Link>
<S.HeaderNav>
<S.HeaderNavItem to="/login" isPage={location.pathname === '/login'}>
로그인
</S.HeaderNavItem>
<S.HeaderNavItem to="/item" isPage={location.pathname === '/item'}>
{userInfo?.user_id ? (
<S.HeaderNavItem to="/" isPage={location.pathname === '/'} onClick={handleLogout}>
로그아웃
</S.HeaderNavItem>
) : (
<S.HeaderNavItem to="/login" isPage={location.pathname === '/login'}>
로그인
</S.HeaderNavItem>
)}
<S.HeaderNavItem
isPage={location.pathname === '/item'}
to={userInfo?.user_id ? '/item' : '/login'}
>
내 밸런스 게임
</S.HeaderNavItem>
<S.HeaderNavItem to="/create" isPage={location.pathname === '/create'}>
<S.HeaderNavItem
isPage={location.pathname === '/create'}
to={userInfo?.user_id ? '/create' : '/login'}
>
밸런스게임만들기
</S.HeaderNavItem>
</S.HeaderNav>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const END_POINTS = {
GET_GAME_ITEMS: (gameId) => `/game/${gameId}/items`,
GET_BEST_COMMENTS: (itemId) => `/item/${itemId}/comments/best`,
GET_COMMENTS: (itemId, page, limit) => `/item/${itemId}/comments?page=${page}&limit=${limit}`,
GET_USER_INFO: '/user',
POST_LOGOUT: '/auth/logout',
POST_LIKE: (commentId) => `/like/${commentId}`,
DELETE_LIKE: (commentId) => `/like/${commentId}`,
};

export default END_POINTS;
1 change: 1 addition & 0 deletions frontend/src/constants/queryKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const QUERY_KEYS = {
GAMES_ITEMS: 'gamesItems',
BEST_COMMENTS: 'bestComments',
COMMENTS: 'comments',
USER_INFO: 'userInfo',
};

export default QUERY_KEYS;
28 changes: 28 additions & 0 deletions frontend/src/context/UserInfoContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import getUserInfo from '../apis/getUserInfo';
import postLogout from '../apis/postLogout';
import { toast } from 'react-toastify';
export const UserInfoContext = createContext({});

export const UserInfoProvider = ({ children }) => {
const [userInfo, setUserInfo] = useState(null);

const logout = useCallback(async () => {
const data = await postLogout();
setUserInfo({});
toast.success(data.message);
}, []);

const login = useCallback(async () => {
const data = await getUserInfo();
setUserInfo(data || {});
}, []);

useEffect(() => {
login();
}, [login]);

return (
<UserInfoContext.Provider value={{ userInfo, logout }}>{children}</UserInfoContext.Provider>
);
};
Loading

0 comments on commit 96647fd

Please sign in to comment.