Team Project [Intermediate] - Implementing a "Like" feature
구현 전 할일
1. 프로젝트 내용과 평가 항목 및 기준 자세히 확인
sns의 뉴스피드 구현하기
프론트 구현 할 필요 없이 API 만 구현
2. 시나리오를 주요 기능을 작성하고 구체적으로 분리
3. figma 로 wireframe 작성
4. ERD 작성 / 타입 설정
5. 시나리오에 맞게 API 명세 작성 - 요청/응답 받을 때 API 포맷이 일관성이 있게
6. 구현 파트 분배 - 팀원의 역량에 맞게
7. 규칙 정하기
1) pr시 확인 절차 필요 - 누가? 몇명이 확인?
2) 브랜치 나누기 - feature, dev, main(배포용)
3) 컨벤션 - 브랜치 이름, 커밋 메세지, 자바 (구글 컨벤션)
https://velog.io/@archivvonjang/Git-Commit-Message-Convention
4) 코드리뷰 언제할지
5) 회의 시간
8. 깃 세팅
1) 대표로 한명이 깃허브에서 repository 또는 organization 생성 (dev, main)
2) 클론
git init
git clone 팀플젝깃허브주소
git checkout dev
git pull
*혹시 마스터로 되어있다면 이거 인텔리제이 터미널에서 추가적으로 작업해서 메인으로 바꿔주기
(맥북은 자동으로 메인으로 바뀌었음)
git remote -v -> 아무것도 안뜨면 원격저장소 아무것도 안떠서 그런것
git remote add origin 팀플젝깃허브주소
git remote -v
git fetch origin
git checkout dev
git pull
3) 브랜치 생성
구현 중 지속적으로 할일
1. 내 브랜치에 코드 작성
2. 터미널에서
git add .
git commit -m "남길내용"
git push origin 브랜치이름
git checkout dev
git pull
git checkout 내 브랜치 이름
git pull origin dev (pr 요청)
깃허브 이동해서 초록 버튼 클릭
왼쪽에 브랜치 main 이라고 써져있는 부분 dev 로 바꿔주기
add title에는 메세지 (ex: feat: like features completed)
merge pull request 누르기
confrim merge
팀원분들께 머지 되었다고 말하고
모두 pull 받기
git checkout dev
git pull
git checkout jh-like_create
git pull origin dev
3. API 응답 json 양식으로 통일하기
https://velog.io/@twoone14/API%EB%8A%94-%EC%9D%91%EB%8B%B5%EC%9D%84-%ED%86%B5%EC%9D%BC%ED%95%98%EC%9E%90
구현 후 할일 (테스트코드 작성 못했을 때)
1) 모두 run 눌러서 잘 실행되는지 확인
2) 포스트맨에서 테스트 확실하게 해보기
3) 데이터 베이스에 잘 되고 있는지 확인
4) 모두가 잘 된다면 main으로 merge 하기
5) 발표 자료 프레젠테이션 준비
6) 핵심기능 시연 영상 촬영
- 동작 원리 / 백엔드 로직 위주로 설명
- ERD나 와이어프레임을 이용해 설명하면 이해를 높일 수 있습니다.
- 핵심 기능 코드 설명
- 코드가 어떤 문제를 해결하는지, 왜 이런 설계를 선택했는지를 이야기해주세요.
7) 트러블 슈팅 정리
- 배경 : 어떤 현상을 발견해서
- 발단 : 이런 장애가 생길 수 있다는 것을 인지했고,
- 전개 : 장애를 대응, 해결하던 와중에
- 위기 : 또 다른 장애 발견 또는 간단하게 해결할 수 없다는 것을 알게되어서,
- 절정 : 근본적인 해결을 위해 이런 방법으로 접근하였다.
- 결말 : 따라서, 이러한 방법을 통해 문제 해결 및 앞으로 유지, 보수에 용이하게 개선하게 되었다.
8) ReadMe 업데이트
https://backendcode.tistory.com/165
- 개발기간
- 멤버구성 및 역할 분담
- 개발 환경
- 주요 기능
- 와이어프레임
- API 명세
- ERD
Controller (작성 후 팀원분께서 수정해주신 코드)
package com.sparta.nbcampnewsfeed.like.cotroller;
import com.sparta.nbcampnewsfeed.auth.annotation.Auth;
import com.sparta.nbcampnewsfeed.auth.dto.requestDto.AuthUser;
import com.sparta.nbcampnewsfeed.like.dto.responseDto.LikePostResponseDto;
import com.sparta.nbcampnewsfeed.like.service.LikeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/posts/{postId}/likes")
public class LikeController {
private final LikeService likeService;
//좋아요 생성
@PostMapping
public ResponseEntity<LikePostResponseDto> likePost(@PathVariable Long postId, @Auth AuthUser authUser) {
LikePostResponseDto responseDto = likeService.likePost(postId, authUser);
return ResponseEntity.ok(responseDto);
}
//좋아요 취소
@DeleteMapping
public String unlikePost(@PathVariable Long postId, @Auth AuthUser authUser) {
likeService.unlikePost(postId, authUser);
return "ok";
}
}
Dto (작성 후 팀원분께서 수정해주신 코드)
package com.sparta.nbcampnewsfeed.like.dto.responseDto;
import com.sparta.nbcampnewsfeed.like.entity.Like;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class LikePostResponseDto {
private Long postId;
private Long userId;
public LikePostResponseDto(Like like) {
this.postId = like.getPost().getPostId();
this.userId = like.getUser().getUserId();
}
}
Entity (작성 후 팀원분께서 수정해주신 코드)
package com.sparta.nbcampnewsfeed.like.entity;
import com.sparta.nbcampnewsfeed.post.entity.Post;
import com.sparta.nbcampnewsfeed.profile.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Getter
@Entity
@NoArgsConstructor
@Table(name = "likes") // mysql 예약어여서 꼭 넣어주기
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long likeId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") //유저정보도 필요하니
private User user;
@ManyToOne(fetch = FetchType.LAZY) //게시글전용 좋아요 기능
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();
public Like(User user, Post post) {
this.user = user;
this.post = post;
}
}
Repository (작성 후 팀원분께서 수정해주신 코드)
package com.sparta.nbcampnewsfeed.like.repository;
import com.sparta.nbcampnewsfeed.like.entity.Like;
import com.sparta.nbcampnewsfeed.post.entity.Post;
import com.sparta.nbcampnewsfeed.profile.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface LikeRepository extends JpaRepository<Like, Long> {
Optional<Like> findByPostAndUser (Post post, User user);
}
Service (작성 후 팀원분께서 수정해주신 코드)
package com.sparta.nbcampnewsfeed.like.service;
import com.sparta.nbcampnewsfeed.auth.dto.requestDto.AuthUser;
import com.sparta.nbcampnewsfeed.like.entity.Like;
import com.sparta.nbcampnewsfeed.post.entity.Post;
import com.sparta.nbcampnewsfeed.profile.entity.User;
import com.sparta.nbcampnewsfeed.like.dto.responseDto.LikePostResponseDto;
import com.sparta.nbcampnewsfeed.like.repository.LikeRepository;
import com.sparta.nbcampnewsfeed.post.repository.PostRepository;
import com.sparta.nbcampnewsfeed.profile.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LikeService {
private final LikeRepository likeRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;
@Transactional
public LikePostResponseDto likePost(Long postId, AuthUser authUser) {
Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("Invalid post ID"));
User user = userRepository.findById(authUser.getId()).orElseThrow(()-> new IllegalArgumentException("Invalid user"));
// 자신이 작성한 게시물에 좋아요를 누를 수 없음
if (post.getUser().getUserId().equals(user.getUserId())) { //로그인한 유저의 아이디와 게시물 작성한 유저의 아이디를 비교하는 것
throw new IllegalArgumentException("You cannot like your own post");
}
// 이미 좋아요를 눌렀는지 확인 //데이터베이스에 포스트아이디랑 회원아이디가 있는지 확인해서 있다면 좋아요를 눌렀다는 증거!
if (likeRepository.findByPostAndUser(post, user).isPresent()) {
throw new IllegalArgumentException("You have already liked this post");
}
// 좋아요 추가
Like like = new Like(user, post);
Like saveLike = likeRepository.save(like);
return new LikePostResponseDto(saveLike);
}
@Transactional
public void unlikePost(Long postId, AuthUser authUser) {
Post post = postRepository.findById(postId).orElseThrow(() -> new IllegalArgumentException("Invalid post ID"));
User user = userRepository.findById(authUser.getId()).orElseThrow(()-> new IllegalArgumentException("Invalid user"));
// 좋아요 기록이 있는지 확인
Like like = likeRepository.findByPostAndUser(post, user).orElseThrow(() -> new IllegalArgumentException("You haven't liked this post yet"));
// 좋아요 취소
likeRepository.delete(like);
}
}
Post Repository (팀원분께서 작성)
package com.sparta.nbcampnewsfeed.post.repository;
import com.sparta.nbcampnewsfeed.post.entity.Post;
import com.sparta.nbcampnewsfeed.profile.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
// 친구들의 게시물을 가져오기 위한 쿼리 (내림차순 정렬)
Page<Post> findAllByUserUserIdInOrderByCreatedAtDesc(List<Long> userIds, Pageable pageable);
List<Post> findAllByUser(User user);
}
Post Controller (팀원분께서 작성)
package com.sparta.nbcampnewsfeed.post.controller;
import com.sparta.nbcampnewsfeed.auth.annotation.Auth;
import com.sparta.nbcampnewsfeed.post.dto.requestDto.PostRequestDto;
import com.sparta.nbcampnewsfeed.post.dto.responseDto.PostResponseDto;
import com.sparta.nbcampnewsfeed.auth.dto.requestDto.AuthUser;
import com.sparta.nbcampnewsfeed.post.service.PostService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/posts")
public class PostController {
public final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
// 게시물 작성
@PostMapping
public ResponseEntity<PostResponseDto> createPost(@Auth AuthUser authUser, @RequestBody PostRequestDto requestDto) {
System.out.println("authUser=" + authUser.getId());
PostResponseDto responseDto = postService.createPost(authUser.getId(), requestDto);
return ResponseEntity.ok(responseDto);
}
// 게시물 수정
@PutMapping("/{postId}")
public ResponseEntity<PostResponseDto> updatePost(@Auth AuthUser authUser, @PathVariable Long postId, @RequestBody PostRequestDto requestDto) {
PostResponseDto responseDto = postService.updatePost(authUser.getId(), postId, requestDto);
return ResponseEntity.ok(responseDto);
}
// 게시물 조회
@GetMapping("/{postId}")
public ResponseEntity<PostResponseDto> getPost(@Auth AuthUser authUser, @PathVariable Long postId) {
PostResponseDto responseDto = postService.getPost(authUser.getId(), postId);
return ResponseEntity.ok(responseDto);
}
// 게시물 삭제
@DeleteMapping("/{postId}")
public ResponseEntity<Void> deletePost(@Auth AuthUser authUser, @PathVariable Long postId) {
postService.deletePost(authUser.getId(), postId);
return ResponseEntity.noContent().build();
}
// 뉴스피드 조회
@GetMapping("/newsfeed")
public ResponseEntity<List<PostResponseDto>> getNewsFeed(@Auth AuthUser authUser,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
List<PostResponseDto> newsfeed = postService.getNewsfeed(authUser.getId(), page - 1, size);
return ResponseEntity.ok(newsfeed);
}
}
User Repository (팀원분께서 작성)
package com.sparta.nbcampnewsfeed.profile.repository;
import com.sparta.nbcampnewsfeed.profile.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
User Controller (팀원분께서 작성)
package com.sparta.nbcampnewsfeed.profile.controller;
import com.sparta.nbcampnewsfeed.auth.annotation.Auth;
import com.sparta.nbcampnewsfeed.auth.dto.requestDto.AuthUser;
import com.sparta.nbcampnewsfeed.profile.dto.requestDto.WithdrawRequestDto;
import com.sparta.nbcampnewsfeed.profile.dto.responseDto.UserProfileMeResponseDto;
import com.sparta.nbcampnewsfeed.profile.dto.responseDto.UserProfileResponseDto;
import com.sparta.nbcampnewsfeed.profile.dto.requestDto.UserProfileUpdateRequestDto;
import com.sparta.nbcampnewsfeed.profile.dto.responseDto.UserProfileUpdateResponseDto;
import com.sparta.nbcampnewsfeed.profile.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// 프로필 조회
@GetMapping("/{userId}/profile")
public ResponseEntity<?> getUserProfile(
@PathVariable Long userId,
@Auth AuthUser authUser) {
if (authUser.getId() != null && authUser.getId().equals(userId)) {
// 자신이 자신의 프로필을 조회하는 경우 모든 정보 반환
UserProfileMeResponseDto responseDto = userService.getUserProfileForMe(userId);
return ResponseEntity.ok(responseDto);
} else {
// 다른 사용자가 조회하는 경우 민감한 정보를 제외한 정보 반환
UserProfileResponseDto responseDto = userService.getUserProfile(userId);
if (responseDto == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found");
}
return ResponseEntity.ok(responseDto);
}
}
// 프로필 수정
@PutMapping("/{userId}/profile")
public ResponseEntity<UserProfileUpdateResponseDto> updateUserProfile(
@PathVariable Long userId,
@RequestBody UserProfileUpdateRequestDto updateRequest,
@Auth AuthUser authUser) {
UserProfileUpdateResponseDto responseDto = userService.updateUserProfile(userId, updateRequest, authUser);
if (responseDto == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(responseDto);
}
// 회원 탈퇴
@DeleteMapping("/withdraw")
public String withdraw(@RequestBody WithdrawRequestDto requestDto,
@Auth AuthUser authUser) {
userService.withdraw(requestDto, authUser);
return "ok";
}
}
출처
깃 커밋 메세지 컨벤션
https://velog.io/@archivvonjang/Git-Commit-Message-Convention
[Git] Commit Message Convention
Git을 협업에 알맞게, 커뮤니케이션에 유용하게, 깔끔한 가독성을 가지도록 사용하기 위해서 좋은 커밋 세미지를 사용하는 것이 중요하다. 그러기 위해서 커밋 컨벤션을 정리하였다.
velog.io
API 응답 json 양식ㅇ로 통일 방법
API는 응답을 통일하자.
위 처럼 응답을 통일해야 프론트측에서 편하게 사용가능하다isSuccess : 응답의 성공, 실패를 좌우!code : http 응답 코드를 반환! HTTP 상태코드 message : Ok또는 실패 메세지를 반환!result : 우리의 진짜
velog.io
ReadME 작성 방법
https://backendcode.tistory.com/165
[Github] README.md 작성하기 - 마크 다운 문법
이번에 GitHub의 README.md 작성법에 대해 정리할 것이다. [목차] - 깃허브에 README.md 파일 생성하기 - 마크다운 문법(MarkDown) 작성 방법 - 다른 사람 README.md 소스 복사하기 예시로 필자가 이전에 작성했
backendcode.tistory.com