Skip to content

Commit

Permalink
[feat #56] 질문글 수정 API (#57)
Browse files Browse the repository at this point in the history
* [chore] : 질문글 추천 API SWAGGER 명세

* [feat] : 질문 수정 응답 dto 추가

* [rename] : 질문 수정 응답 dto 컨벤션에 맞게 네이밍 변경

* [refactor] : dto 팩토리 메서드 제거

* [feat] : 질문글 업데이트 dto 추가

* [fix] : dto mapper에 이미지, url null일 때 검증 로직 추가

* [feat] : 이미지 일괄 삭제를 위한 repository 추가

* [feat] : 업데이트 dto mapper 함수 추가

* [feat] : 엔티티 내 필드 업데이트 로직 추가

* [feat] : 질문글 업데이트 비즈니스 로직 작성

* [feat] : 질문글 업데이트 API 메서드 작성

* [test] : 요구사항 validation 맞게 fixture 수정

* [fix] : 이미지 삭제 시 질문글 내 이미지 리스트 비우는 로직 추가

* [fix] : 컨트롤러 누락된 어노테이션 추가

* [fix] : 리스트 데이터 변경 불가 예외 해결

* [test] : 질문글 업데이트 단위 테스트 작성

* [test] : 질문글 업데이트 통합 테스트 작성

* [style] : 코드 리포멧팅

* [feat] : null 대신 새 ArrayList 할당
  • Loading branch information
hyun2371 authored Aug 19, 2024
1 parent 7c43f08 commit 4faba8d
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -14,10 +15,12 @@
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;
import com.dnd.gongmuin.question_post.dto.response.RecQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.UpdateQuestionPostResponse;
import com.dnd.gongmuin.question_post.service.QuestionPostService;

import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -66,6 +69,8 @@ public ResponseEntity<PageResponse<QuestionPostSimpleResponse>> searchQuestionPo
return ResponseEntity.ok(response);
}

@Operation(summary = "질문글 추천 API", description = "직군에 맞는 질문글을 추천순으로 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/api/question-posts/recommends")
public ResponseEntity<PageResponse<RecQuestionPostResponse>> getRecommendQuestionPosts(
@AuthenticationPrincipal Member member,
Expand All @@ -75,4 +80,16 @@ public ResponseEntity<PageResponse<RecQuestionPostResponse>> getRecommendQuestio
= questionPostService.getRecommendQuestionPosts(member, pageable);
return ResponseEntity.ok(response);
}

@Operation(summary = "질문글 업데이트 API", description = "질문자가 질문글을 업데이트 한다.")
@ApiResponse(useReturnTypeSchema = true)
@PatchMapping("/api/question-posts/{questionPostId}/edit")
public ResponseEntity<UpdateQuestionPostResponse> updateQuestionPosts(
@PathVariable("questionPostId") Long questionPostId,
@Valid @RequestBody UpdateQuestionPostRequest request
) {
UpdateQuestionPostResponse response
= questionPostService.updateQuestionPost(questionPostId, request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,30 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class QuestionPost extends TimeBaseEntity {

@OneToMany(mappedBy = "questionPost", cascade = CascadeType.ALL)
private final List<QuestionPostImage> images = new ArrayList<>();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "question_post_id", nullable = false)
private Long id;

@Column(name = "title", nullable = false)
private String title;

@Column(name = "content", nullable = false)
private String content;

@Column(name = "reward", nullable = false)
private int reward;

@Enumerated(EnumType.STRING)
@Column(name = "job_group", nullable = false)
private JobGroup jobGroup;

@Column(name = "is_chosen", nullable = false)
private Boolean isChosen;

@OneToMany(mappedBy = "questionPost", cascade = CascadeType.ALL)
private List<QuestionPostImage> images = new ArrayList<>();

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id",
nullable = false,
Expand All @@ -66,7 +73,7 @@ private QuestionPost(String title, String content, int reward, JobGroup jobGroup
this.reward = reward;
this.jobGroup = jobGroup;
this.member = member;
addImages(images);
initPostImages(images);
}

public static QuestionPost of(String title, String content, int reward, JobGroup jobGroup,
Expand All @@ -75,13 +82,27 @@ public static QuestionPost of(String title, String content, int reward, JobGroup
}

//==양방향 편의 메서드==//
private void addImages(List<QuestionPostImage> images) {
private void initPostImages(List<QuestionPostImage> images) {
images.forEach(image -> {
this.images.add(image);
image.addQuestionPost(this);
});
}

public void updatePostImages(List<String> imageUrls) {
List<QuestionPostImage> questionPostImages = new ArrayList<>();
imageUrls.stream().map(QuestionPostImage::from)
.forEach(questionPostImage -> {
questionPostImage.addQuestionPost(this);
questionPostImages.add(questionPostImage);
});
this.images = questionPostImages;
}

public void clearPostImages() {
this.images.clear();
}

public boolean isQuestioner(Long memberId) {
return Objects.equals(this.member.getId(), memberId);
}
Expand All @@ -92,4 +113,13 @@ public void updateIsChosen(Answer answer) {
this.isChosen = true;
answer.updateIsChosen();
}

public void updateQuestionPost(
String title, String content, int reward, JobGroup jobGroup
) {
this.title = title;
this.content = content;
this.reward = reward;
this.jobGroup = jobGroup;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dnd.gongmuin.question_post.dto;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.dnd.gongmuin.member.domain.JobGroup;
Expand All @@ -10,6 +12,7 @@
import com.dnd.gongmuin.question_post.dto.response.MemberInfo;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.UpdateQuestionPostResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand All @@ -19,9 +22,7 @@ public class QuestionPostMapper {

public static QuestionPost toQuestionPost(RegisterQuestionPostRequest request, Member member) {
JobGroup jobGroup = JobGroup.from(request.targetJobGroup());
List<QuestionPostImage> images = request.imageUrls().stream()
.map(QuestionPostImage::from)
.toList();
List<QuestionPostImage> images = urlsToImages(request);
return QuestionPost.of(request.title(), request.content(), request.reward(), jobGroup, images, member);
}

Expand All @@ -35,8 +36,7 @@ public static QuestionPostDetailResponse toQuestionPostDetailResponse(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
questionPost.getImages().stream()
.map(QuestionPostImage::getImageUrl).toList(),
imagesToUrls(questionPost.getImages()),
questionPost.getReward(),
questionPost.getJobGroup().getLabel(),
new MemberInfo(
Expand All @@ -50,16 +50,15 @@ public static QuestionPostDetailResponse toQuestionPostDetailResponse(
);
}

public static RegisterQuestionPostResponse toQuestionPostDetailResponse(
public static RegisterQuestionPostResponse toRegisterQuestionPostResponse(
QuestionPost questionPost
) {
Member member = questionPost.getMember();
return new RegisterQuestionPostResponse(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
questionPost.getImages().stream()
.map(QuestionPostImage::getImageUrl).toList(),
imagesToUrls(questionPost.getImages()),
questionPost.getReward(),
questionPost.getJobGroup().getLabel(),
new MemberInfo(
Expand All @@ -70,4 +69,35 @@ public static RegisterQuestionPostResponse toQuestionPostDetailResponse(
questionPost.getCreatedAt().toString()
);
}

public static UpdateQuestionPostResponse toUpdateQuestionPostResponse(
QuestionPost questionPost
) {
return new UpdateQuestionPostResponse(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
imagesToUrls(questionPost.getImages()),
questionPost.getReward(),
questionPost.getJobGroup().getLabel()
);
}

private static List<QuestionPostImage> urlsToImages(RegisterQuestionPostRequest request) {
List<QuestionPostImage> images = new ArrayList<>();
if (request.imageUrls() != null) {
images = request.imageUrls().stream()
.map(QuestionPostImage::from)
.toList();
}
return images;
}

private static List<String> imagesToUrls(List<QuestionPostImage> images) {
if (images == null)
return Collections.emptyList();
return images.stream()
.map(QuestionPostImage::getImageUrl)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,4 @@ public record RegisterQuestionPostRequest(
@NotBlank(message = "직군을 입력해주세요.")
String targetJobGroup
) {
public static RegisterQuestionPostRequest of(
String title,
String content,
List<String> imageUrls,
int reward,
String targetJobGroup
) {
return new RegisterQuestionPostRequest(
title, content, imageUrls, reward, targetJobGroup
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.dnd.gongmuin.question_post.dto.request;

import java.util.List;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record UpdateQuestionPostRequest(

@Size(min = 2, max = 20, message = "제목은 2자 이상 20자 이하여야 합니다.")
String title,

@Size(min = 10, max = 200, message = "본문은 10자 이상 200자 이하여야 합니다.")
String content,

List<String> imageUrls,

@Min(value = 2_000, message = "리워드는 2000 이상이어야 합니다.")
@Max(value = 10_000, message = "리워드는 10000 이하여야 합니다.")
int reward,

@NotBlank(message = "직군을 입력해주세요.")
String targetJobGroup
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.gongmuin.question_post.dto.response;

import java.util.List;

public record UpdateQuestionPostResponse(
Long questionPostId,
String title,
String content,
List<String> imageUrls,
int reward,
String targetJobGroup
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dnd.gongmuin.question_post.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.domain.QuestionPostImage;

public interface QuestionPostImageRepository extends JpaRepository<QuestionPostImage, Long> {
void deleteByQuestionPost(QuestionPost questionPost);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dnd.gongmuin.question_post.service;

import java.util.List;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
Expand All @@ -9,6 +11,7 @@
import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.member.domain.JobGroup;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.exception.MemberErrorCode;
import com.dnd.gongmuin.post_interaction.domain.InteractionCount;
Expand All @@ -18,11 +21,14 @@
import com.dnd.gongmuin.question_post.dto.QuestionPostMapper;
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;
import com.dnd.gongmuin.question_post.dto.response.RecQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.UpdateQuestionPostResponse;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostImageRepository;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

import lombok.RequiredArgsConstructor;
Expand All @@ -32,8 +38,17 @@
public class QuestionPostService {

private final QuestionPostRepository questionPostRepository;

private final InteractionCountRepository interactionCountRepository;
private final QuestionPostImageRepository questionPostImageRepository;

private static void updateQuestionPost(UpdateQuestionPostRequest request, QuestionPost questionPost) {
questionPost.updateQuestionPost(
request.title(),
request.content(),
request.reward(),
JobGroup.from(request.targetJobGroup())
);
}

@Transactional
public RegisterQuestionPostResponse registerQuestionPost(
Expand All @@ -44,7 +59,7 @@ public RegisterQuestionPostResponse registerQuestionPost(
throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT);
}
QuestionPost questionPost = QuestionPostMapper.toQuestionPost(request, member);
return QuestionPostMapper.toQuestionPostDetailResponse(
return QuestionPostMapper.toRegisterQuestionPostResponse(
questionPostRepository.save(questionPost)
);
}
Expand Down Expand Up @@ -80,6 +95,32 @@ public PageResponse<RecQuestionPostResponse> getRecommendQuestionPosts(
return PageMapper.toPageResponse(responsePage);
}

@Transactional
public UpdateQuestionPostResponse updateQuestionPost(
Long questionPostId,
UpdateQuestionPostRequest request
) {
QuestionPost questionPost = questionPostRepository.findById(questionPostId)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
updateQuestionPostImages(questionPost, request.imageUrls());
updateQuestionPost(request, questionPost);
return QuestionPostMapper.toUpdateQuestionPostResponse(questionPost);
}

private void updateQuestionPostImages(QuestionPost questionPost, List<String> imageUrls) {
if (imageUrls != null) { // 수정 사항 존재
deleteImages(questionPost); // 기존 이미지 객체 삭제 (새로 비우기 || 수정할 값 존재)
if (!imageUrls.isEmpty()) { //수정할 값 담아보냄
questionPost.updatePostImages(imageUrls);
}
}
}

private void deleteImages(QuestionPost questionPost) {
questionPostImageRepository.deleteByQuestionPost(questionPost);
questionPost.clearPostImages();
}

private int getCountByType(Long questionPostId, InteractionType type) {
return interactionCountRepository
.findByQuestionPostIdAndType(questionPostId, type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public class QuestionPostFixture {
public static QuestionPost questionPost(Member member) {
return QuestionPost.of(
"제목",
"내용",
1000,
"내용내용내용내용내용",
2000,
JobGroup.from("공업"),
List.of(
QuestionPostImage.from("image1.jpg"),
Expand Down
Loading

0 comments on commit 4faba8d

Please sign in to comment.