Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/main/java/com/example/FixLog/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.FixLog.controller;

import com.example.FixLog.dto.post.NewPostRequestDto;
import com.example.FixLog.dto.post.PostRequestDto;
import com.example.FixLog.dto.Response;
import com.example.FixLog.dto.post.PostResponseDto;
Expand All @@ -16,30 +17,43 @@ public PostController(PostService postService){
this.postService = postService;
}

// 게시글 작성하기
@PostMapping
public Response<Object> createPost(@RequestBody PostRequestDto postRequestDto){
postService.createPost(postRequestDto);
return Response.success("게시글 작성 성공.", null);
}

// 이미지 마크다운 형식으로 변환하기
@PostMapping("/images")
public Response<String> uploadImage(@RequestPart("imageFile") MultipartFile imageFile){
String markdownImage = postService.uploadImage(imageFile);
return Response.success("이미지 마크다운 형식으로 변환", markdownImage);
}

// 게시글 수정하기
@PatchMapping("/{postId}/edit")
public Response<Object> editPost(@PathVariable("postId") Long postId,
@RequestBody NewPostRequestDto newPostRequestDto){
postService.editPost(postId, newPostRequestDto);
return Response.success("게시글 수정 성공.", null);
}

// 게시글 보기
@GetMapping("/{postId}")
public Response<Object> viewPost(@PathVariable("postId") Long postId){
PostResponseDto viewPost = postService.viewPost(postId);
return Response.success("게시글 조회하기 성공", viewPost);
}

// 좋아요 누르기/취소하기
@PostMapping("/{postId}/like")
public Response<Object> togglePostLike(@PathVariable("postId") Long postId){
String message = postService.togglePostLike(postId);
return Response.success(message, null); // 좋아요 수정하기
}

// 북마크 누르기/취소하기
@PostMapping("/{postId}/bookmark")
public Response<Object> toggleBookmark(@PathVariable("postId") Long postId) {
String message = postService.toggleBookmark(postId);
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/example/FixLog/domain/post/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,41 @@ public class Post {
@OneToMany(mappedBy = "postId", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostLike> postLikes = new ArrayList<>();

public void clearTags(){
postTags.clear();
}

public void changeTitle(String newTitle){
this.postTitle = newTitle;
}
Comment on lines +86 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

필드 검증 로직 추가 권장

현재 setter 메서드들이 검증 없이 값을 직접 설정합니다.
최소한 필수 필드에 대한 null/blank 검증을 추가하는 것이 좋습니다.

 public void changeTitle(String newTitle){
+    if (newTitle == null || newTitle.isBlank()) {
+        throw new IllegalArgumentException("제목은 비어있을 수 없습니다");
+    }
+    if (newTitle.length() > 100) {
+        throw new IllegalArgumentException("제목은 100자를 초과할 수 없습니다");
+    }
     this.postTitle = newTitle;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void changeTitle(String newTitle){
this.postTitle = newTitle;
}
public void changeTitle(String newTitle){
if (newTitle == null || newTitle.isBlank()) {
throw new IllegalArgumentException("제목은 비어있을 수 없습니다");
}
if (newTitle.length() > 100) {
throw new IllegalArgumentException("제목은 100자를 초과할 수 없습니다");
}
this.postTitle = newTitle;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/FixLog/domain/post/Post.java around lines 86 to 88,
the changeTitle method sets the postTitle field without validation. Add
validation logic to check that newTitle is not null or blank before assigning it
to postTitle, and handle invalid input appropriately, such as throwing an
IllegalArgumentException.

public void changeCoverImage(String newCoverImage){
this.coverImage=newCoverImage;
}
public void changeProblem(String newProblem){
this.problem = newProblem;
}
public void changeErrorMessage(String newErrorMessage){
this.errorMessage = newErrorMessage;
}
public void changeEnvironment(String newEnvironment){
this.environment = newEnvironment;
}
public void changeReproduceCode(String newReproduceCode){
this.reproduceCode = newReproduceCode;
}
public void changeSolutionCode(String newSolutionCode){
this.solutionCode = newSolutionCode;
}
public void changeCauseAnalysis(String newCauseAnalysis){
this.causeAnalysis = newCauseAnalysis;
}
public void changeReferenceLink(String newReferenceLink){
this.referenceLink = newReferenceLink;
}
public void changeExtraContent(String newExtraContent){
this.extraContent = newExtraContent;
}
public void updateEditedAt(LocalDateTime newLocalDateTime){
this.editedAt = newLocalDateTime;
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/example/FixLog/dto/post/NewPostRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.FixLog.dto.post;

import lombok.Getter;

import java.util.List;

@Getter
public class NewPostRequestDto {
private String postTitle;
private String coverImageUrl;
private String problem;
private String errorMessage;
private String environment;
private String reproduceCode;
private String solutionCode;
private String causeAnalysis;
private String referenceLink;
private String extraContent;

private List<Long> tags;
}
Comment on lines +8 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

DTO 레벨 검증 추가 권장

현재 DTO에 검증 어노테이션이 없어 모든 검증을 서비스 계층에서 수행해야 합니다.
필수 필드에 대한 기본적인 검증을 DTO 레벨에서 수행하면 더 견고한 코드가 됩니다.

+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;

 @Getter
 public class NewPostRequestDto {
+    @NotBlank(message = "제목은 필수입니다")
+    @Size(max = 100, message = "제목은 100자 이내여야 합니다")
     private String postTitle;
     private String coverImageUrl;
+    @NotBlank(message = "문제 설명은 필수입니다")
     private String problem;
+    @NotBlank(message = "에러 메시지는 필수입니다")
     private String errorMessage;
+    @NotBlank(message = "환경 정보는 필수입니다")
     private String environment;
+    @NotBlank(message = "재현 코드는 필수입니다")
     private String reproduceCode;
+    @NotBlank(message = "해결 코드는 필수입니다")
     private String solutionCode;
     private String causeAnalysis;
     private String referenceLink;
     private String extraContent;
 
+    @NotNull(message = "태그는 필수입니다")
+    @Size(min = 1, message = "최소 하나의 태그를 선택해야 합니다")
     private List<Long> tags;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class NewPostRequestDto {
private String postTitle;
private String coverImageUrl;
private String problem;
private String errorMessage;
private String environment;
private String reproduceCode;
private String solutionCode;
private String causeAnalysis;
private String referenceLink;
private String extraContent;
private List<Long> tags;
}
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Getter
public class NewPostRequestDto {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 100, message = "제목은 100자 이내여야 합니다")
private String postTitle;
private String coverImageUrl;
@NotBlank(message = "문제 설명은 필수입니다")
private String problem;
@NotBlank(message = "에러 메시지는 필수입니다")
private String errorMessage;
@NotBlank(message = "환경 정보는 필수입니다")
private String environment;
@NotBlank(message = "재현 코드는 필수입니다")
private String reproduceCode;
@NotBlank(message = "해결 코드는 필수입니다")
private String solutionCode;
private String causeAnalysis;
private String referenceLink;
private String extraContent;
@NotNull(message = "태그는 필수입니다")
@Size(min = 1, message = "최소 하나의 태그를 선택해야 합니다")
private List<Long> tags;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/FixLog/dto/post/NewPostRequestDto.java between
lines 8 and 21, the DTO lacks validation annotations, requiring all validation
to be done in the service layer. Add appropriate validation annotations such as
@NotNull or @NotBlank on mandatory fields to enforce basic validation at the DTO
level, improving code robustness and reducing service layer validation burden.

3 changes: 2 additions & 1 deletion src/main/java/com/example/FixLog/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public enum ErrorCode {
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다."),
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "요청 데이터가 유효하지 않습니다."),
S3_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "S3 파일 업로드에 실패했습니다."),
IMAGE_UPLOAD_FAILED(HttpStatus.NOT_FOUND, "이미지 파일이 업로드되지 않았습니다.");
IMAGE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "이미지 파일이 업로드되지 않았습니다."),
NO_CONTENT_CHANGED(HttpStatus.BAD_REQUEST, "변경된 내용이 없습니다.");

private final HttpStatus status;
private final String message;
Expand Down
87 changes: 74 additions & 13 deletions src/main/java/com/example/FixLog/service/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.example.FixLog.domain.post.PostTag;
import com.example.FixLog.domain.tag.Tag;
import com.example.FixLog.domain.tag.TagCategory;
import com.example.FixLog.dto.post.NewPostRequestDto;
import com.example.FixLog.dto.post.PostDto;
import com.example.FixLog.dto.post.PostRequestDto;
import com.example.FixLog.dto.post.PostResponseDto;
Expand Down Expand Up @@ -62,7 +63,7 @@ public String getDefaultImage(String image){
return imageUrl;
}

// 게시글 생성하기
// 게시글 작성하기
@Transactional
public void createPost(PostRequestDto postRequestDto){
Member member = memberService.getCurrentMemberInfo();
Expand Down Expand Up @@ -99,18 +100,6 @@ public void createPost(PostRequestDto postRequestDto){
postRepository.save(newPost);
}

// 이미지 파일 마크다운으로 변경
public String uploadImage(MultipartFile imageFile){
SecurityContextHolder.getContext().getAuthentication();

if (imageFile == null || imageFile.isEmpty()){
throw new CustomException(ErrorCode.IMAGE_UPLOAD_FAILED);
}

String imageUrl = s3Service.upload(imageFile, "post-image");
return "![image](" + imageUrl + ")";
}

// 태그 다 선택 했는지
private List<Tag> fetchAndValidateTags(List<Long> tagIds){
// 태그 ID로 Tag 엔티티 조회
Expand Down Expand Up @@ -160,6 +149,78 @@ private void validatePost(PostRequestDto postRequestDto){
| postRequestDto.getReproduceCode().isBlank() | postRequestDto.getSolutionCode().isBlank())
throw new CustomException(ErrorCode.REQUIRED_CONTENT_MISSING);
}
private void validatePost(NewPostRequestDto newPostRequestDto){
if (newPostRequestDto.getPostTitle().isBlank() | newPostRequestDto.getProblem().isBlank()
| newPostRequestDto.getErrorMessage().isBlank() | newPostRequestDto.getEnvironment().isBlank()
| newPostRequestDto.getReproduceCode().isBlank() | newPostRequestDto.getSolutionCode().isBlank())
throw new CustomException(ErrorCode.REQUIRED_CONTENT_MISSING);
}

// 이미지 파일 마크다운으로 변경
@Transactional
public String uploadImage(MultipartFile imageFile){
SecurityContextHolder.getContext().getAuthentication();

if (imageFile == null || imageFile.isEmpty()){
throw new CustomException(ErrorCode.IMAGE_UPLOAD_FAILED);
}

String imageUrl = s3Service.upload(imageFile, "post-image");
return "![image](" + imageUrl + ")";
}

@Transactional
public void editPost(Long postId, NewPostRequestDto newPostRequestDto) {
Member member = memberService.getCurrentMemberInfo();
Post post = postRepository.findById(postId)
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));

Comment on lines +173 to +177
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중대한 보안 문제: 작성자 권한 검증 누락

현재 코드는 로그인한 사용자 정보만 가져올 뿐, 게시글 작성자와 비교하지 않습니다.
누구나 다른 사람의 게시글을 수정할 수 있는 심각한 보안 취약점이 있습니다.

 @Transactional
 public void editPost(Long postId, NewPostRequestDto newPostRequestDto) {
     Member member = memberService.getCurrentMemberInfo();
     Post post = postRepository.findById(postId)
             .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
+    
+    // 게시글 작성자 확인
+    if (!post.getUserId().equals(member)) {
+        throw new CustomException(ErrorCode.ACCESS_DENIED);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void editPost(Long postId, NewPostRequestDto newPostRequestDto) {
Member member = memberService.getCurrentMemberInfo();
Post post = postRepository.findById(postId)
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
@Transactional
public void editPost(Long postId, NewPostRequestDto newPostRequestDto) {
Member member = memberService.getCurrentMemberInfo();
Post post = postRepository.findById(postId)
.orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND));
// 게시글 작성자 확인
if (!post.getUserId().equals(member)) {
throw new CustomException(ErrorCode.ACCESS_DENIED);
}
// ... the rest of your update logic ...
}
🤖 Prompt for AI Agents
In src/main/java/com/example/FixLog/service/PostService.java around lines 173 to
177, the code fetches the current logged-in user but does not verify if this
user is the author of the post being edited. To fix this critical security
issue, add a check comparing the current member's ID with the post author's ID
after retrieving the post. If they do not match, throw an appropriate exception
to prevent unauthorized edits.

// 북마크 카테고리별로 선택 제한 두기
List<Tag> tags = fetchAndValidateTags(newPostRequestDto.getTags());

// 아무것도 변경이 없으면 예외처리
if (Objects.equals(post.getPostTitle(), newPostRequestDto.getPostTitle())
& Objects.equals(post.getCoverImage(), newPostRequestDto.getCoverImageUrl())
& Objects.equals(post.getProblem(), newPostRequestDto.getProblem())
& Objects.equals(post.getErrorMessage(), newPostRequestDto.getErrorMessage())
& Objects.equals(post.getEnvironment(), newPostRequestDto.getEnvironment())
& Objects.equals(post.getReproduceCode(), newPostRequestDto.getReproduceCode())
& Objects.equals(post.getSolutionCode(), newPostRequestDto.getSolutionCode())
& Objects.equals(post.getCauseAnalysis(), newPostRequestDto.getCauseAnalysis())
& Objects.equals(post.getReferenceLink(), newPostRequestDto.getReferenceLink())
& Objects.equals(post.getExtraContent(), newPostRequestDto.getExtraContent())
& compareTags(post.getPostTags(), tags)){
throw new CustomException(ErrorCode.NO_CONTENT_CHANGED);
}

// 필드 업데이트
post.changeTitle(newPostRequestDto.getPostTitle());
post.changeCoverImage(newPostRequestDto.getCoverImageUrl());
post.changeProblem(newPostRequestDto.getProblem());
post.changeErrorMessage(newPostRequestDto.getErrorMessage());
post.changeEnvironment(newPostRequestDto.getEnvironment());
post.changeReproduceCode(newPostRequestDto.getReproduceCode());
post.changeSolutionCode(newPostRequestDto.getSolutionCode());
post.changeCauseAnalysis(newPostRequestDto.getCauseAnalysis());
post.changeReferenceLink(newPostRequestDto.getReferenceLink());
post.changeExtraContent(newPostRequestDto.getExtraContent());
post.updateEditedAt(LocalDateTime.now());

// 태그 저장
post.clearTags(); // 기존 태그 다 제거
for (Tag tag : tags) {
PostTag postTag = new PostTag(post, tag);
post.getPostTags().add(postTag);
}
}

private boolean compareTags(List<PostTag> currentPostTags, List<Tag> newTags) {
List<Tag> currentTags = currentPostTags.stream()
.map(PostTag::getTagId)
.toList();

return new HashSet<>(currentTags).equals(new HashSet<>(newTags));
}

// 게시글 조회하기
public PostResponseDto viewPost(Long postId){
Expand Down