팀원
- 이예지(팀장)
마이페이지 구현, 머스테치 수정 - 이지은
테이블 매핑, 테이블 구현/수정 - 이경은
좋아요 기능 구현 - github 주소
https://github.com/GDGoC-3lee
선택한 기능 및 구현 계획
1. 좋아요 기능
- 좋아요 하트 버튼 최초 클릭 시(좋아요 등록 시) Like_Table에 좋아요가 등록되고 해당 좋아요의 liked값이 true가 된다.
- 좋아요 취소 시 Like_Table에 등록되었던 좋아요의 liked값이 false가 된다.
- 다시 해당 글에 좋아요를 클릭 시 좋아요의 liked값이 true가 된다.
2. 마이페이지
- 마이페이지 경로로 진입 시 현재 로그인한 유저의 이름, 이메일, 프로필 사진을 보여준다.
핵심코드
1. 테이블 매핑
- ERD기반 Posts, Like_Table, User테이블 매핑
- Posts, Like_table 1:N mapping
- User, Like_table 1:N mapping
- User, Posts 1:N mapping
//posts에게 참조함
public class User extends BaseTimeEntity {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
//한 명의 유저 여러 개의 게시글
private List<Posts> posts=new ArrayList<>();
//POSTS 담을 arraylist 필요
//user로부터 참조받음
public class Posts extends BaseTimeEntity{
@ManyToOne(fetch = FetchType.LAZY)
//여러 개의 게시글 한 명의 유저
//fetch: user가 로드될 때 posts 리스트 즉시 로드되지 않음. 필요할 때 로드됨
@JoinColumn(name="user_id")
//참조할 때 컬럼명
private User user;
// user 가져오기
2. 좋아요 기능
- LikeService
- toggleLike: 좋아요를 클릭하면 Like_Table의 liked 값을 true↔false로 바꾸는 기능
- isLiked: 현재 좋아요 상태 확인
package com.jojoldu.book.springboot.service.like;
import com.jojoldu.book.springboot.domain.posts.Posts;
import com.jojoldu.book.springboot.domain.user.User;
import com.jojoldu.book.springboot.domain.like.Like_Table;
import com.jojoldu.book.springboot.domain.like.LikeRepository;
import com.jojoldu.book.springboot.service.posts.PostsService;
import com.jojoldu.book.springboot.service.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class LikeService {
private final LikeRepository likeRepository;
private final PostsService postsService;
private final UserService userService;
@Transactional
public Long toggleLike(Long post_id, String username) {
Posts posts = postsService.findById(post_id).toEntity();
User user = userService.findByUsername(username).toEntity();
Like_Table likeEntity = likeRepository.findByPostsAndUser(posts, user)
.orElse(null);
if (likeEntity == null) {
likeEntity = Like_Table.builder()
.posts(posts)
.user(user)
.liked(true) // 처음에는 false로 생성됨
.build();
likeRepository.save(likeEntity);
} else {
likeEntity.update(!likeEntity.isLiked());
}
return likeEntity.getLike_id();
}
// 좋아요 상태 확인
public boolean isLiked(Long post_id, String username) {
Posts post = postsService.findById(post_id).toEntity();
User user = userService.findByUsername(username).toEntity();
Like_Table likeEntity = likeRepository.findByPostsAndUser(post, user)
.orElse(null);
return likeEntity != null && likeEntity.isLiked();
}
}
- PostsApiController
//좋아요 상태 확인 ------
@GetMapping("/{id}/like")
public boolean getLikeStatus(@PathVariable Long id) {
SessionUser sessionUser = (SessionUser) httpSession.getAttribute("user");
if (sessionUser != null) {
String username = sessionUser.getName();
return likeService.isLiked(id, username);
}
return false; //로그인 X
}
@PutMapping("/{id}/like")
public ResponseEntity<Long> toggleLike(@PathVariable Long id) {
String username = null;
SessionUser sessionUser = (SessionUser) httpSession.getAttribute("user");
if (sessionUser != null) {
username = sessionUser.getName();
}
Long likeId = likeService.toggleLike(id, username);
// 상태 변경 후 like_id 반환
return ResponseEntity.ok(likeId);
}
3. 마이페이지
- IndexController
@GetMapping("/mypage")
public String myPage(Model model, @LoginUser SessionUser user) {
if (user != null) {
model.addAttribute("userName", user.getName());
model.addAttribute("userEmail", user.getEmail());
model.addAttribute("userPicture", user.getPicture());
}
return "mypage";
}
API 명세서
1. 좋아요 등록, 취소
HTTP Method | PUT |
Parameter | @PathVariable Long |
Endpoint | /{id}/like |
2. 좋아요 상태 조회
HTTP Method | GET |
Parameter | @PathVariable Long |
Endpoint | /{id}/like |
3. 마이페이지
HTTP Method | GET |
Parameter | Model, SessionUser |
Endpoint | /mypage |
트러블슈팅
- 게시글 등록 시 에러 발생
- 상황: 게시글을 등록하기 위해서 h2-console에서 ROLE을 USER로 변경해준 뒤 게시글 작성 시 에러 발생
- 원인: Like 테이블 이름이 SQL 예약어와 충돌
- 해결: 테이블명을 Like_Table로 변경
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "ALTER TABLE LIKE[*] ADD CONSTRAINT FK8OIGKSXPJDXGAS3G45U4BA88B FOREIGN KEY (USER_ID) REFERENCES USER (ID)"; expected "identifier"; SQL statement:
alter table like add constraint FK8oigksxpjdxgas3g45u4ba88b foreign key (user_id) references user (id) [42001-200]
- 매핑 후 빌드 에러
- 상황: User와 Like_table 1:N, Posts와 Like_table 1:N 매핑 후 build 불가
- 원인: User와 Posts 에서 매핑 시에 list를 생성하는데, 두 class에서 list 명을 동일하게 하여 참조 위치를 분명하게 하지 못해 두 개의 선택지가 생김
- 해결: list 앞에 final(고정값)을 붙여서 리스트의 값은 바뀔 수 있게 하되 참조하는 객체는 매번 동일하게 만듦
//User, Posts.java 파일 변경
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<Like_Table> likeTable =new ArrayList<>();
//final: 객체 참조만 고정, 내부 데이터는 변경 가능
- JSON parse error
- 상황: JSON parse error: Cannot deserialize value of type long from String "…”
- 원인: LikeTable의 user_id(int타입)에 머스태치(화면)의 #user값(username/long타입)을 넣음
- 해결:
- UserService에서 UserRepository를 통해 username으로 User entity 찾기
User entity=userRepository.findByName(username).
orElseThrow(()->new IllegalArgumentException("해당 사용자가 없습니다. name="+username));
- PostsApiController에서 현재 로그인한 사용자 이름 받아와서 LikeService에 post_id와 username 전달
return likeService.save(requestDto.getPost_id(), username);
- LikeService에서 LikeRepository에 해당 user entity와 post entity를 속성값으로 가지는 likeEntity 생성 후 Like Table에 삽입
Like_Table likeEntity = Like_Table.builder().posts(posts).user(user).build();
- 마우스 떼면 하트 색 바뀌는 에러
- 상황: 좋아요 누를 때만 빨간 하트였다가 마우스를 떼면 바로 빈 하트가 됨
- 원인: 화면이 새로고침 되면서 다시 렌더링됨
- 해결: 새로고침해도 좋아요 상태가 남아있도록 함
- 첫 번째 시도: 세션스토리지(로컬스토리지) 이용
- 여전히 새로고침하면 빈 하트로 바뀌어서 페이지가 로드될 때(새로고침할 때)마다 로컬스토리지에 저장된 값을 읽어오도록 함
initializeLikes: function () {
var _this = this;
$('.likeBtn').each(function () {
const likeId = $(this).data('like-id');
const liked = (localStorage.getItem(likeId) === 'true');
const heart = $(this).find('.heart');
→ 위 문제는 해결되었지만, IntelliJ에서 Application을 재실행하고 localhost:8080에 접속할 때 이전에 실행했을 때 로컬스토리지에 넣어놓은 값이 초기화되지 않고 유지됨
→ 새로고침할 때는 유지되고 Application 새로 실행했을 때만 초기화되도록 하는 것이 목표
- 두 번째 시도: 서버에서 데이터 불러와서 사용
- LikeService에 좋아요 상태를 확인하는 isLiked() 메소드 추가
public boolean isLiked(Long post_id, String username) {
Posts post = postsService.findById(post_id).toEntity();
User user = userService.findByUsername(username).toEntity();
return likeRepository.existsByPostsAndUser(post, user);
//post에 user가 좋아요 눌렀는지 확인
}
@GetMapping("/{id}/like")
public boolean getLikeStatus(@PathVariable Long id){
//현재 로그인한 사용자
SessionUser sessionUser = (SessionUser) httpSession.getAttribute("user");
if (sessionUser != null) {
String username = sessionUser.getName();
return likeService.isLiked(id, username);
}
return false; //로그인 X 경우
}
GET 요청을 통해 받아온 좋아요 상태값에 따라 렌더링
$(document).ready(function () {
$('.likeBtn').each(function (){
var post_id = $(this).data('like-id');
var $heart = $(this).find('.heart');
$.ajax({
url:'api/v1/posts/' + post_id + '/like',
type: 'GET',
success: function (isLiked) {
if(isLiked) {
$heart.css('color', 'red');
$heart.text('♥');
} else {
$heart.css('color', 'gray');
$heart.text('♡');
}
},
error: function() {
alert('좋아요 상태를 가져오는 데 실패했습니다.');
}
})
})
})
→ 첫 번째 좋아요 정상 등록, 정상 취소되지만 두 번째 좋아요 등록했다가 취소시 좋아요가 없다고 나오는 문제 발생(좋아요 취소 에러: id=2인 좋아요가 아닌 id=1인 좋아요를 삭제하려고 하는 문제)
- 좋아요 취소 에러
- 상황: 첫 번째 좋아요 정상 등록, 첫 번째 좋아요 정상 취소, 두 번째 좋아요 정상 등록, 두 번째 좋아요 취소 에러
Hibernate: select like_table0_.like_id as like_id1_0_0_, like_table0_.created_date as created_2_0_0_, like_table0_.modified_date as modified3_0_0_, like_table0_.post_id as post_id4_0_0_, like_table0_.user_id as user_id5_0_0_ from like_table like_table0_ where like_table0_.like_id=?
2024-12-04 03:12:29.548 ERROR 39685 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: 해당 좋아요가 없습니다. id=1] with root cause
java.lang.IllegalArgumentException: 해당 좋아요가 없습니다. id=1
- 에러 발생 이유 추측
- 좋아요 등록이랑 취소가 경로가 둘다 "/api/v1/posts" 뒤에 추가적으로 붙은 게 없어서 중간에 혼동이 생겼을 가능성
→ 좋아요 등록 메소드 post로 변경하고 경로도 /api/v1/posts/{id}/like, /api/v1/posts/{id}/unlike로 각각 수정한 뒤에 실행해도 같은 오류 발생 - Like_Table과 Post 테이블 간의 참조 관계로 인한 오류
- index.mustache에서 좋아요 버튼 id = 게시글 id로 사용 중, 즉 data-like-id가 게시글 번호와 같은 변수를 가리키고 있어서 문제가 발생할 가능성도 있음
- 좋아요 등록이랑 취소가 경로가 둘다 "/api/v1/posts" 뒤에 추가적으로 붙은 게 없어서 중간에 혼동이 생겼을 가능성
- 우회하여 해결한 방법
- like_table에 boolean liked값 추가해서 true false로 좋아요 여부 구분
- 좋아요 등록, 취소 모두 PUT 사용
느낀점
이예지
교재를 통해서 스터디를 진행할 때는 이론은 이해가 되지만, 실제로 어떻게 사용되는지 체감하기가 어려운 부분이 있었는데, 실제로 특정 기능을 처음부터 구현하려고 하다보니까 자연스럽게 전체적인 이해도가 높아질 수 있었습니다. dto, controller, service 간의 관계를 파악할 수 있었을 뿐만 아니라 직접 erd를 설계하고 역할을 분담하는 과정에서 백엔드 개발 과정에 대한 궁금증을 해소할 수 있었습니다. 비록 직접 토큰을 생성하고 유저를 식별하여 유저의 정보를 받아오는 것을 구현하지는 못했지만, 앞으로 있을 심화 프로젝트들의 발판을 마련할 수 있었던 의미있는 시간이었다고 생각합니다.
이지은
기존의 스터디 교재를 통한 결과물에 새로운 기능을 추가하면서 기존 코드를 한번 더 보며 배운 내용을 복습할 수 있어 유익했습니다. 특히 service에서 시작해 dto, controller까지 이어지는 흐름의 순서를 like 테이블을 구현하면서 복기하고 받아들일 수 있었습니다. 비록 코드 상의 이유로 postman이나 swagger를 이용해 api 연결을 확인해보는 과정까지는 구현하지 못했으나, 머스테치를 통해 프론트 기능까지 구현해 백과 프론트를 아우르는 하나의 완성된 프로젝트를 구현하게 되어 전반적인 프로젝트의 프로세스를 익힐 수 있는 시간이었습니다.
이경은
스터디 과정에서는 코드나 구조/계층의 설계와 활용이 다소 추상적으로 느껴졌지만, 프로젝트를 진행하며 이를 실제로 구현하고 응용해보며 이해도를 높일 수 있었습니다. 또한 백엔드 로직과 협업 과정을 익히고 경험해보며 많은 배움을 얻었습니다. 다만 제가 작성했던 코드가 최적화 측면에서 부족한 점이 있었던 것 같아 아쉬움이 남습니다. 이후에 스프링에 대해 더 깊이 공부해보고, 완성도 있는 코드를 작성하는 연습을 많이 해보며 이를 기반으로 더욱 발전된 프로젝트를 진행해보고 싶습니다.
'Group Study (2024-2025) > Spring 입문' 카테고리의 다른 글
[Spring 입문] Toy Project - Team 하와수 (1) | 2024.12.04 |
---|---|
[Spring 입문] 6주차 - EC2 서버에 프로젝트를 배포해 보자 (3) | 2024.11.10 |
[Spring 입문] 5주차 - AWS 서버 환경을 만들어보자 - AWS RDS (7장) (1) | 2024.11.05 |
[Spring 입문] 5주차 - AWS 서버 환경을 만들어보자 - AWS EC2 (4) | 2024.11.03 |
[Spring 입문] 4주차 - 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (1) (2) | 2024.10.29 |