팀원 및 저장소
- 하은수(팀장)
- 마이페이지 구현
- 리펙터링: 게시글 유저 매핑
- https://github.com/ha9eun
- 이수연
- 팔로우 기능 구현
- 리펙터링: 회원가입 유저 권한, 게시글 수정/삭제 권한 변경
- https://github.com/choubung
- Github Organization
선택한 기능 및 구현 계획
- 팔로우
- 홈화면(게시글 목록 보기 페이지)에서 작성자 옆의 'follow' 버튼을 누르면,
- 위의 액션을 취한 로그인 유저 팔로잉 목록에 작성자가 추가되고
- 게시글 작성자의 팔로워 목록에 로그인 유저가 추가된다.
- 팔로우가 성공적으로 완료되면 'follow' 버튼이 'unfollow' 버튼으로 변경된다.
- * 자기 자신의 글에는 팔로우 버튼이 뜨지 않는다.
- 마이페이지
- 내 정보
- 이름
- 이메일 주소
- 프로필 사진
- 내가 쓴 글
- 목록으로 보여주며, 제목과 작성 날짜를 표시한다.
- 제목을 클릭하면 글 상세 페이지로 이동한다.
- 팔로우 및 팔로잉 목록
- 계정의 이름과 이메일을 표시한다.
- 내 정보
- 기존 코드 리팩터링
- 회원가입 유저 기본 role을 USER로 설정
- 기존에는 기본 role이 GUEST로, 글 작성/수정/삭제 권한이 없었다.
- 회원가입 후 별다른 절차 없이 바로 글을 작성/수정/삭제할 수 있다.
- 게시글과 유저 매핑
- 기존에는 게시글에 유저가 매핑되어 있지 않아 작성자를 직접 작성했다.
- 로그인한 유저에 맞게 작성자가 자동으로 설정되도록 변경했다.
- 게시글을 자기 자신만 수정할 수 있도록 변경
- 기존에는 자신이 작성한 글이 아니어도 수정 및 삭제가 가능했다.
- 로그인한 사용자와 게시글 작성자가 같을 때만 수정 및 삭제가 가능하도록 변경했다.
- 회원가입 유저 기본 role을 USER로 설정
작동 화면
<팔로우 기능>
<마이페이지>
기능을 구현하기 위해 조사하며 얻은 인사이트
- 팔로우
- 인사이트
- 각 팔로우 기능(팔로우, 언팔로우, 팔로잉/팔로워 조회)에 맞는 HTTP 메소드
- Controller와 Service를 분리하는 형태
- 인터페이스가 레포지토리를 구현하는 방식 (스프링 스터디 때 모호하게 알고 있던 걸 이해함)
- 레퍼런스 링크
- https://velog.io/@simhw/JPA-%ED%8C%94%EB%A1%9C%EC%9A%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
- https://z1-colab.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A1%9C-%ED%8C%94%EB%A1%9C%EC%9A%B0%ED%8C%94%EB%A1%9C%EC%9E%89-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90#%F0%9F%9A%A9%20Entity-1
- https://green-bin.tistory.com/118
- 인사이트
- 마이페이지
API 명세서
<팔로우 API 명세서>
- 팔로우 - followerID 사용자가 followeeId 사용자를 팔로우
- HTTP Method: [POST]
- Endpoint: /api/follow/{followerId}/follow/{followeeId}
- Request Parameters
- followerId (Path, Long): 팔로우를 요청하는 사용자 ID
- followeeId (Path, Long): 팔로우 대상 사용자 ID
- Response
- HTTP 200 OK: 팔로우 성공
- HTTP 400 Bad Request: 잘못된 요청
- 예시
- POST /api/follow/1/follow/2
- 언팔로우 - followerId 사용자가 followeeId 사용자를 언팔로우
- HTTP Method: [DELETE]
- Endpoint: /api/follow/{followerId}/unfollow/{followeeId}
- Request Parameters
- followerId (Path, Long): 언팔로우를 요청하는 사용자 ID
- followeeId (Path, Long): 언팔로우 대상 사용자 ID
- Response
- HTTP 200 OK: 팔로우 성공
- HTTP 400 Bad Request: 잘못된 요청
- 예시
- DELETE /api/follow/1/follow/2
- 팔로잉 목록 조회 - userId 사용자가 팔로우하고 있는 사용자 목록을 조회
- HTTP Method: [GET]
- Endpoint: /api/follow/{userId}/following
- Request Parameters
- userId (Path, Long): 팔로잉 목록을 조회할 사용자 ID
- Response
- HTTP 200 OK: 팔로우 성공
- HTTP 400 Bad Request: 잘못된 요청
- Response Body
- [ { "id": 1, "followerId": 1, "followeeId": 2 }, { "id": 2, "followerId": 1, "followeeId": 3 } ]
- 예시
- DELETE /api/follow/1/follow/2
- 팔로워 목록 조회 - userId 사용자를 팔로우하고 있는 사용자 목록을 조회
- HTTP Method: [GET]
- Endpoint: /api/follow/{userId}/followers
- Request Parameters
- userId (Path, Long): 팔로워 목록을 조회할 사용자 ID
- Response
- HTTP 200 OK: 팔로우 성공
- HTTP 400 Bad Request: 잘못된 요청
- Response Body
- [ { "id": 3, "followerId": 2, "followeeId": 1 }, { "id": 4, "followerId": 3, "followeeId": 1 } ]
- 예시
- DELETE /api/follow/1/follow/2
<마이페이지 API 명세서>
- HTTP Method: [GET]
- Endpoint: /mypage
- Response
- HTTP 200 OK
- HTML 페이지 렌더링 (my-page 뷰)
- Response Body
- {
"userName": "이수연",
"userEmail": "suyeonlee@example.com",
"userPicture": "https://example.com/images/suyeonlee.jpg"
}
- {
핵심 코드
<팔로우 기능>
package com.jojoldu.book.springboot.domain.follow;
import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import com.jojoldu.book.springboot.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Follow extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "followee_id")
private User followee; // 내가 팔로잉 하는 사람
@ManyToOne
@JoinColumn(name = "follower_id")
private User follower; // 나의 팔로워
public Long getId() {
return id;
}
public User getFollowee() {
return followee;
}
public User getFollower() {
return follower;
}
}
- ↑ follow 엔티티
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.service.FollowService;
import com.jojoldu.book.springboot.web.dto.FollowDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/follow")
public class FollowController {
private final FollowService followService;
@PostMapping("/{followerId}/follow/{followeeId}")
public ResponseEntity<String> follow(@PathVariable Long followerId, @PathVariable Long followeeId) {
try {
followService.follow(followerId, followeeId);
return ResponseEntity.ok("Followed successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error following user");
}
}
@DeleteMapping("/{followerId}/unfollow/{followeeId}")
public ResponseEntity<String> unfollow(@PathVariable Long followerId, @PathVariable Long followeeId) {
try {
followService.unfollow(followerId, followeeId);
return ResponseEntity.ok("Unfollowed successfully");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error unfollowing user");
}
}
@GetMapping("/{userId}/following")
public ResponseEntity<List<FollowDto>> getFollowing(@PathVariable Long userId) {
List<FollowDto> following = followService.getFollowing(userId);
return ResponseEntity.ok(following);
}
@GetMapping("/{userId}/followers")
public ResponseEntity<List<FollowDto>> getFollowers(@PathVariable Long userId) {
List<FollowDto> followers = followService.getFollowers(userId);
return ResponseEntity.ok(followers);
}
}
- ↑ FollowController
package com.jojoldu.book.springboot.service;
import com.jojoldu.book.springboot.domain.follow.Follow;
import com.jojoldu.book.springboot.domain.follow.FollowRepository;
import com.jojoldu.book.springboot.domain.user.User;
import com.jojoldu.book.springboot.domain.user.UserRepository;
import com.jojoldu.book.springboot.web.dto.FollowDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class FollowService {
private final FollowRepository followRepository;
private final UserRepository userRepository;
public boolean isFollowing(Long followerId, Long followeeId) {
return followRepository.existsByFollowerIdAndFolloweeId(followerId, followeeId);
}
@Transactional
public void follow(Long followerId, Long followeeId) {
User follower = userRepository.findById(followerId)
.orElseThrow(() -> new IllegalArgumentException("Invalid follower ID: " + followerId));
User followee = userRepository.findById(followeeId)
.orElseThrow(() -> new IllegalArgumentException("Invalid followee ID: " + followeeId));
Follow follow = Follow.builder()
.follower(follower)
.followee(followee)
.build();
followRepository.save(follow);
}
@Transactional
public void unfollow(Long followerId, Long followeeId) {
Follow follow = followRepository.findByFollowerIdAndFolloweeId(followerId, followeeId)
.orElseThrow(() -> new IllegalArgumentException("Follow relationship not found"));
followRepository.delete(follow);
}
@Transactional(readOnly = true)
public List<FollowDto> getFollowing(Long userId) {
return followRepository.findByFollowerId(userId).stream()
.map(FollowDto::new)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<FollowDto> getFollowers(Long userId) {
return followRepository.findByFolloweeId(userId).stream()
.map(FollowDto::new)
.collect(Collectors.toList());
}
}
- ↑ FollowService
<마이페이지 기능>
package com.jojoldu.book.springboot.web;
import com.jojoldu.book.springboot.config.auth.LoginUser;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.service.FollowService;
import com.jojoldu.book.springboot.service.posts.PostsService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@RequiredArgsConstructor
@Controller
public class MyPageController {
private final PostsService postsService;
private final FollowService followService;
@GetMapping("/mypage")
public String myPage(Model model, @LoginUser SessionUser user) {
model.addAttribute("userName", user.getName());
model.addAttribute("userEmail", user.getEmail());
model.addAttribute("userPicture", user.getPicture());
model.addAttribute("posts", postsService.findPostsByUser(user.getEmail()));
model.addAttribute("followings", followService.getFollowing(user.getId()));
model.addAttribute("followers", followService.getFollowers(user.getId()));
return "my-page";
}
}
- ↑ MyPageController
트러블 슈팅
<인텔리제이 Run 불가 오류 >
- 문제 상황
- 인텔리제이가 spring 패키지를 인식하지 못해 application을 실행할 수 없음
- 문제 원인과 해결 시도
- JDK 버전 충돌: 원격 저장소의 원본 코드는 JDK 20, 저장소 코드를 pull한 로컬 환경은 JDK 17 사용
→ 로컬 JDK 버전을 20으로 올림 - 루트 폴더가 (ToyProject/하은수/)freelec-springboot2-webservice가 아닌 ToyProject로 설정되어 gradle 등 필요한 파일 인식 불가
→ 루트 폴더를 변경하고 새로 빌드 - 실행>실행/디버그 구성에 Spring Boot 구성이 존재하지 않음
→ Spring Boot로 Application 추가
- JDK 버전 충돌: 원격 저장소의 원본 코드는 JDK 20, 저장소 코드를 pull한 로컬 환경은 JDK 17 사용
<마이페이지 버튼 구현 후 팔로우/언팔로우 기능 먹통 버그>
- 문제상황
- 홈에 마이페이지 버튼을 구현한 뒤, 팔로우/언팔로우 기능이 오작동함
- 문제 원인과 해결 시도
- 머스테치에 숨겨서 보냈던 userID 정보가 삭제되어 동적 팔로우/언팔로우 구현 관련 머스테치에서 오류가 발생
→ 누락된 코드를 복원해 해결
- 머스테치에 숨겨서 보냈던 userID 정보가 삭제되어 동적 팔로우/언팔로우 구현 관련 머스테치에서 오류가 발생
회고
하은수
- 교재를 보며 열심히 공부하고 실습하여 배포까지 성공했지만 와닿지 않는 내용이 많았습니다.
토이프로젝트를 위해 기존 코드를 파악할 때 비로소 전체적인 구조와 세부적인 코드를 잘 이해할 수 있었습니다.
특히 유저와 글 작성자를 매핑하면서 연관관계 매핑을 추가적으로 공부하게 되어 유익했습니다.
다만 시간이 부족해 테스트 코드를 작성하는 연습을 해보지 못한 것이 아쉬움으로 남습니다.
이번 스터디를 통해 스프링에 대한 흥미가 커졌고, 방학동안 더 깊게 공부해보려고 합니다.
이수연
- 교재를 따라하며 스프링부트 게시판 프로젝트를 완성하긴 했지만, 코드를 완전히 이해하진 못했어서 토이프로젝트를 처음 시작할 때 원래 코드를 이해하느라 시간을 좀 소모했습니다. 토이 프로젝트를 통해 모호하게 이해했던 개념들(왜 컨트롤러와 서비스를 분리하는지 등)을 조금이나마 체화할 수 있었고, 특히 팔로우 구현 과정에서 mapped by를 사용하는 법을 새로이 알게 되었습니다. 구조를 이해하니 확실히 코드를 보는 것과 쓰는 게 수월해서 종강하면 조금 더 본격적으로 스프링 공부를 진행하고 싶다는 생각이 들었습니다.
'Group Study (2024-2025) > Spring 입문' 카테고리의 다른 글
[Spring 입문] Toy Project - Team 3lee (1) | 2024.12.21 |
---|---|
[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 |