어제 만든 erd와 api에 대해 튜터님한테 피드백이 왔다.
erd는 내가 만들었는데 피드백이 많이 와서 하나하나 보니 다 이해가 가는 문장들이었다
더욱 꼼꼼하게 보고 erd를 구현했어야 됐는데.. 다음에 구현할 때는 처음부터 똑바로 짜야겠다
API
1. 담당자가 적혀있어 좋습니다.
2. 반환 데이터에 에러 경우도 적혀있어 좋습니다.
3. 데이터 타입이 적혀있어 좋습니다.
4. contents, comment 단수 복수가 섞여있어서 통일하는 것이 좋습니다.
5. 스프링 시큐리티를 사용하고 필터를 사용하게 되면 validate API가 별도로 필요하지 않을 것으로 보이는데 존재하는 이유가 있을까요? 테스트를 위해서라면 다른 API에 작성된 것처럼 '테스트용'이 있으면 명확합니다.
6. 사진 업로드를 하는 함수를 재사용 가능하게 만들고 API는 별도로 분리하지 않아도 될 것 같습니다.
6-1. 만약 분리를 하는 것이 맞다고 판단되면 적어도
API url은 file이 아닌 사진 기능이라는 걸 알 수 있는 조금 더 직관적인 이름이 좋겠습니다.
7. 사진 다운로드는 요구사항에 없기 때문에 일단 우선순위에서 미루는 것이 좋겠습니다.
ERD
1. NewsFeed_id, commentCount, likeCount만 이름 규칙이 다릅니다.
2. 데이터 타입을 varchar로 설정을 할 때 길이도 명확하게 설정해 주세요.
3. 테이블 이름을 대문자로 시작하기로 결정했다면 profile 테이블 이름도 맞춰야겠습니다.
4. User, Profile 테이블을 분리했다면 email은 한 테이블에만 있어도 될 것으로 보입니다.
5. picture_url 데이터 타입에 오타가 있습니다.
6. NewsFeedLike가 NewsFeed_id를 가지고 있는데 CommentLike는 댓글과 연결점이 없습니다.
7. user_id가 외래 키로 걸려있는 테이블에서 user name을 한 번 더 저장을 하는 이유가 무엇일까요?
역할분담
1. 역할 분담을 완료하셨는데 전체적인 설계 담당은 누가 하는지
그리고 테스트는 어떻게 분배할 것인지도 고려해 보면 좋겠습니다.
스코프 적정성
1. 모든 기능을 구현하는 것으로 보입니다. 개발 시간을 계산을 했을 때
가능하다고 판단을 하셨는지 아니면 일단 도전을 해보시는 것인지 궁금합니다.
튜터님의 피드백이다. 정말 잘 주신 거 같다. 피드백에 맞게 erd랑 api도 바꿨다
일단 나는 댓글 쪽을 맡았다.
회원가입하고 로그인해서 게시글을 쓰고 그 게시글에 댓글을 다는 기능이다.
댓글 기능은 저번 개인과제 때도 비슷한 맥락으로 했고
api와 erd로 보고 하면 되니깐 크게 어렵지 않았다
그래서 내 거 기능을 끝내고 다른 팀원분들이 아직 못한 기능이나 힘들어하시는 거 있으면
같이 도움을 드리려 했는데 .. 거의 24시간 만에 90% 이상 구현을 해서
물론 나도 못하지만 내가 도움드릴건 없었다
그래서 제일 마지막에 있는
6. HTTP를 HTTPS로 업그레이드하기
HTTPS를 적용하여 보안이 강화된 웹 페이지를 제공해 봅시다.
이 부분을 한번 해보려고 한번 찾아봤지만 매우 복잡했다. 일단 도메인이 필요하고
배포를 해야 되는데 .. 깃허브로 배포하면 자동으로 https이다.
그리고 정확한 방법이 쓰여있지 않고 딱 저렇게 쓰여있어서 감도 못잡았다.
1. 도메인 등록
2. ssl 인증서 발급
3. 인증서 설치하고 서버 설정
4. https로 설정 이긴 하다... 일단은 이거 빼고 진행하는 걸로
그리고 깃허브에서 issues라는 기능? 버튼? 을 처음 써봤는데 매우 좋은 거 같다.
한눈에 볼 수 있고, 어떤 기능이 진행 중이거나 끝난 게 바로 보인다
라벨도 사용 가능하다!
내가 맡은 댓글 관련 코드와 설명이다.
api와 erd는 위에 참고하면 된다
원래 개인 프로젝트 할 때는 주석을 잘 안다는데
팀 프로젝트는 내가 한 기능이나 다른 사람이 한 기능을 딱 보면 모르니
주석이 필수다. 주석도 잘 작성해야 팀 프로젝트도 원활하게 진행이 가능하다!!
/**
* CommentRequestDto는 뉴스피드에 댓글을 추가하는 데 필요한 데이터를 담는 DTO
* 댓글 내용과 해당 뉴스피드의 ID를 포함
*/
@Getter
@Setter
public class CommentRequestDto {
@NotNull(message = "뉴스피드 ID는 필수입니다.")
private Long newsfeedId; // 댓글이 속할 뉴스피드의 ID
@NotBlank(message = "댓글 내용은 공백일 수 없습니다.")
@Size(max = 255, message = "댓글은 최대 255자까지 입력 가능합니다.")
private String content; // 댓글 내용
}
//RequestDto
새로운 댓글 작성하거나 기존 댓글 수정할 때 클라이언트로부터 입력받는 데이터를 담는 dto
제약조건도 걸어줬다.
/**
* CommentResponseDto는 댓글 조회 응답 시 반환되는 데이터를 담는 DTO
* 이 DTO는 댓글의 ID, 뉴스피드 ID, 작성자 ID, 내용, 생성 및 수정 시간을 포함
*/
@Getter
public class CommentResponseDto {
private Long id; // 댓글 ID
private Long newsfeedId; // 뉴스피드 ID
private Long userId; // 댓글 작성자 ID
private String content; // 댓글 내용
private String createdAt; // 댓글 생성 시간
private String modifiedAt; // 댓글 수정 시간
public CommentResponseDto(Comment comment) {
this.id = comment.getId();
this.newsfeedId = comment.getNewsfeedId();
this.userId = comment.getUserId();
this.content = comment.getContent();
this.createdAt = formatDateTime(comment.getCreatedAt());
this.modifiedAt = formatDateTime(comment.getModifiedAt());
}
// 날짜 형식을 보기 좋게 포매팅하는 메서드
private String formatDateTime(java.time.LocalDateTime dateTime) {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
//ResponseDto
댓글 조회 시 클라이언트에게 반환되는 데이터를 담는 dto
/**
* Comment 엔티티는 뉴스피드에 대한 댓글을 나타낸다.
* 이 클래스는 댓글의 ID, 뉴스피드 ID, 사용자 ID, 댓글 내용을 관리!
*/
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Comment extends TimeStamp {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 댓글의 고유 ID
private Long newsfeedId; // 댓글이 속한 뉴스피드의 ID
private Long userId; // 댓글을 작성한 사용자의 ID
private String content; // 댓글 내용
/**
* 댓글 생성자
* @param newsfeedId 뉴스피드의 ID
* @param userId 댓글 작성자의 사용자 ID
* @param content 댓글의 내용
*/
public Comment(Long newsfeedId, Long userId, String content) {
this.newsfeedId = newsfeedId;
this.userId = userId;
this.content = content;
}
/**
* 댓글 수정 시간을 설정하는 메서드
* @param modifiedAt 수정 시간
*/
public void setModifiedAt(LocalDateTime modifiedAt) {
try {
Field field = TimeStamp.class.getDeclaredField("modifiedAt");
field.setAccessible(true);
field.set(this, modifiedAt);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace(); //timestamp에 setter추가하기 고려
}
}
}
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeStamp {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
//Comment entity , timestamp를 상속받았다
댓글을 나타내며 관리한다.
/**
* CommentController는 뉴스피드의 댓글 관련 요청을 처리
* 댓글의 생성, 조회, 수정, 삭제 기능
*/
@RestController
@RequestMapping("/newsfeed/{newsfeedId}/comment")
public class CommentController {
@Autowired
private CommentService commentService;
/**
* 댓글을 생성
* @param newsfeedId 댓글이 작성될 뉴스피드 ID
* @param requestDto 댓글 작성 데이터
* @param token 사용자 인증 토큰
* @return 생성된 댓글 정보 또는 에러 메시지
*/
@PostMapping
public ResponseEntity<?> createComment(@PathVariable Long newsfeedId,
@RequestBody CommentRequestDto requestDto,
@RequestHeader("Authorization") String token) {
try {
CommentResponseDto response = commentService.addComment(newsfeedId, requestDto, token);
return new ResponseEntity<>(response, HttpStatus.CREATED);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body("댓글 내용을 입력하지 않았습니다.");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 유효하지 않습니다.");
}
}
/**
* 지정된 뉴스피드의 모든 댓글을 조회
* @param newsfeedId 댓글을 조회할 뉴스피드 ID
* @param token 사용자 인증 토큰
* @return 댓글 목록 또는 에러 메시지
*/
@GetMapping
public ResponseEntity<?> getAllComments(@PathVariable Long newsfeedId,
@RequestHeader("Authorization") String token) {
try {
List<CommentResponseDto> responses = commentService.getAllComments(newsfeedId, token);
return ResponseEntity.ok(responses);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("유효하지 않은 뉴스피드 ID입니다.");
}
}
/**
* 지정된 댓글을 수정
* @param newsfeedId 댓글이 위치한 뉴스피드의 ID
* @param commentId 댓글 ID
* @param requestDto 수정할 댓글 데이터
* @param token 사용자 인증 토큰
* @return 수정된 댓글 정보 또는 에러 메시지
*/
@PutMapping("/{commentId}")
public ResponseEntity<?> updateComment(@PathVariable Long newsfeedId,
@PathVariable Long commentId,
@RequestBody CommentRequestDto requestDto,
@RequestHeader("Authorization") String token) {
try {
CommentResponseDto response = commentService.updateComment(newsfeedId, commentId, requestDto, token);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("유효하지 않은 댓글 ID입니다.");
}
}
/**
* 지정된 댓글을 삭제
* @param newsfeedId 댓글이 위치한 뉴스피드의 ID
* @param commentId 삭제할 댓글 ID
* @param token 사용자 인증 토큰
* @return 삭제 성공 메시지 또는 에러 메시지
*/
@DeleteMapping("/{commentId}")
public ResponseEntity<?> deleteComment(@PathVariable Long newsfeedId,
@PathVariable Long commentId,
@RequestHeader("Authorization") String token) {
try {
commentService.deleteComment(newsfeedId, commentId, token);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("유효하지 않은 댓글 ID입니다.");
}
}
}
//Controller
댓글 생성, 조회, 수정, 삭제 기능!
/**
* CommentRepository는 댓글 엔티티에 대한 데이터베이스 작업을 수행하는 레포지토리
*/
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByNewsfeedId(Long newsfeedId);
Optional<Comment> findByIdAndNewsfeedIdAndUserId(Long commentId, Long newsfeedId, Long userId);
}
//Repository
댓글 엔티티에 대한 db 작업을 수행하는 레포지토리
JPaRepository를 상속받아 기본적인 crud 기능!
/**
* CommentService는 댓글 관련 비즈니스 로직을 처리
*/
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtil jwtUtil;
/**
* 뉴스피드에 댓글을 추가
* @param newsfeedId 뉴스피드 ID
* @param requestDto 댓글 작성 요청 DTO
* @param token JWT 토큰
* @return 생성된 댓글의 응답 DTO
*/
@Transactional
public CommentResponseDto addComment(Long newsfeedId, CommentRequestDto requestDto, String token) {
Long userId = validateToken(token);
Comment comment = new Comment(newsfeedId, userId, requestDto.getContent());
comment = commentRepository.save(comment);
return new CommentResponseDto(comment);
}
/**
* 뉴스피드의 모든 댓글을 조회
* @param newsfeedId 뉴스피드 ID
* @param token JWT 토큰
* @return 댓글 응답 DTO 리스트
*/
@Transactional(readOnly = true)
public List<CommentResponseDto> getAllComments(Long newsfeedId, String token) {
validateToken(token);
List<Comment> comments = commentRepository.findByNewsfeedId(newsfeedId);
return comments.stream()
.map(CommentResponseDto::new)
.collect(Collectors.toList());
}
/**
* 특정 댓글을 수정
* @param newsfeedId 뉴스피드 ID
* @param commentId 댓글 ID
* @param requestDto 댓글 수정 요청 DTO
* @param token JWT 토큰
* @return 수정된 댓글의 응답 DTO
*/
@Transactional
public CommentResponseDto updateComment(Long newsfeedId, Long commentId, CommentRequestDto requestDto, String token) {
Long userId = validateToken(token);
Comment comment = commentRepository.findByIdAndNewsfeedIdAndUserId(commentId, newsfeedId, userId)
.orElseThrow(() -> new CommentNotFoundException("해당 댓글이 존재하지 않거나 권한이 없습니다."));
comment.setContent(requestDto.getContent());
comment.setModifiedAt(LocalDateTime.now());
comment = commentRepository.save(comment);
return new CommentResponseDto(comment);
}
/**
* 특정 댓글을 삭제
* @param newsfeedId 뉴스피드 ID
* @param commentId 댓글 ID
* @param token JWT 토큰
*/
@Transactional
public void deleteComment(Long newsfeedId, Long commentId, String token) {
Long userId = validateToken(token);
Comment comment = commentRepository.findByIdAndNewsfeedIdAndUserId(commentId, newsfeedId, userId)
.orElseThrow(() -> new CommentNotFoundException("해당 댓글이 존재하지 않거나 권한이 없습니다."));
commentRepository.delete(comment);
}
/**
* 토큰을 검증하고 사용자 ID를 반환하는 메소드
* @param token JWT 토큰
* @return 사용자 ID
*/
private Long validateToken(String token) {
String username = jwtUtil.getUsernameFromToken(token.replace("Bearer ", ""));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UnauthorizedException("인증 정보가 유효하지 않습니다."));
return user.getId();
}
}
//Service
비지니스 로직 처리
/**
* 댓글이 존재하지 않거나 접근 권한이 없는 경우 발생하는 예외
*/
public class CommentNotFoundException extends RuntimeException {
public CommentNotFoundException(String message) {
super(message);
}
}
// Exception
댓글이 존재하지 않거나 접근 권한이 없는 경우 발생하는 예외 클래스
일단 이렇게 구현을 했다 ~
다음 주 화요일까지 프로젝트 기간이니 더 완성도 있게 만들어야겠다.
'TIL' 카테고리의 다른 글
TIL - 2024/06/10 (0) | 2024.06.10 |
---|---|
TIL - 2024/06/07 (0) | 2024.06.07 |
TIL - 2024/06/04 (0) | 2024.06.04 |
TIL - 2024/06/03 (0) | 2024.06.03 |
TIL - 2024/05/31 (0) | 2024.05.31 |