Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Q&A 기능 추가 #526

Merged
merged 64 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
dc709f1
feat: 질문과 답변에 대한 엔티티 추가
JJ503 Oct 3, 2023
df75dd7
feat: 질문 레포지토리 추가
JJ503 Oct 3, 2023
73a4b19
feat: 질문 등록 서비스 추가
JJ503 Oct 3, 2023
26078dd
feat: 질문 등록 api 추가
JJ503 Oct 3, 2023
4b7fb84
test: QuestionService 테스트 추가
JJ503 Oct 3, 2023
e14af04
feat: 답변 레포지토리 추가
JJ503 Oct 3, 2023
d81ff04
feat: 답변 등록 서비스 추가
JJ503 Oct 3, 2023
e0e6e8e
refactor: 컨트롤러 클래스 명 수정
JJ503 Oct 3, 2023
0d5f85f
feat: 답변 등록 api 추가
JJ503 Oct 3, 2023
ab415a0
feat: 질문 및 답변 조회 레파지토리 추가
JJ503 Oct 3, 2023
f54d0a9
feat: 질문 및 답변 조회 서비스 추가
JJ503 Oct 3, 2023
59fb738
rename: 하위 패키지 생성
JJ503 Oct 3, 2023
4224840
feat: 질문과 답변 목록 전체 조회 api 추가
JJ503 Oct 4, 2023
29076c3
test: 테스트 픽스처 접근 제어자 설정
JJ503 Oct 4, 2023
97aa6ea
docs: 고민 todo 추가
JJ503 Oct 4, 2023
12c6e3d
feat: 질문과 답변관련 flyway 스크립트 추가
JJ503 Oct 4, 2023
7cfcd3a
docs: api 문서 최신화
JJ503 Oct 4, 2023
0146a4e
refactor: 불필요한 개행 제거
JJ503 Oct 4, 2023
68c9497
feat: 경매 조회시 삭제된 경매를 제외해주는 메서드 추가
JJ503 Oct 4, 2023
3559bcd
refactor: 경매 조회시 삭제된 경매는 존재하지 않는 경매로 처리되도록 수정
JJ503 Oct 4, 2023
66b18d6
refactor: 개행 추가
JJ503 Oct 5, 2023
e35adcb
test: 불필요한 코드 제거
JJ503 Oct 5, 2023
636d934
test: 컨벤션에 맞춰 개행
JJ503 Oct 5, 2023
1d7e896
style: todo 제거
JJ503 Oct 5, 2023
1611cff
refactor: 예외처리를 적절한 커스텀 예외로 변경
JJ503 Oct 5, 2023
98ac03b
rename: QuestionAndAnswer를 Qna로 축약해 사용
JJ503 Oct 5, 2023
544ad4c
ci: 브랜치 최신화
JJ503 Oct 5, 2023
493260b
ci: 브랜치 최신화 충돌 문제 해결
JJ503 Oct 5, 2023
ea5df21
feat: 질문 삭제 기능 추가
JJ503 Oct 5, 2023
410426b
feat: 질문 삭제 기능 서비스 추가
JJ503 Oct 5, 2023
082d7d5
feat: 질문 삭제 기능 api 추가
JJ503 Oct 5, 2023
e33718c
refactor: 삭제된 질문은 조회되지 않도록 수정
JJ503 Oct 5, 2023
ce53640
test: 메서드 네이밍 수정
JJ503 Oct 5, 2023
5866b56
feat: 답변 삭제 기능 서비스 추가
JJ503 Oct 5, 2023
0dba066
feat: 답변 삭제 api 추가
JJ503 Oct 5, 2023
a1b178a
feat: 질문과 답변에 삭제 여부 필드 추가
JJ503 Oct 6, 2023
4da8433
feat: 질문 신고 엔티티, 레파지토리 추가
JJ503 Oct 6, 2023
16e2029
feat: 트랜잭션 어노테이션 추가
JJ503 Oct 6, 2023
7c50d90
feat: 질문 신고 등록 서비스 추가
JJ503 Oct 6, 2023
ae7751b
feat: 질문 신고 등록 api 추가
JJ503 Oct 6, 2023
4912784
feat: 질문 신고 조회 레포지토리 추가
JJ503 Oct 6, 2023
5a5896f
feat: 질문 신고 조회 서비스 추가
JJ503 Oct 6, 2023
ec7fdca
feat: 질문 신고 조회 api 추가
JJ503 Oct 6, 2023
0f0d3e9
feat: 답변 신고 엔티티, 레포지토리 추가
JJ503 Oct 6, 2023
1771054
feat: 답변 신고 등록 서비스 추가
JJ503 Oct 6, 2023
026d62d
feat: 답변 신고 등록 api 추가
JJ503 Oct 6, 2023
c06b726
feat: 답변 신고 조회 레포지토리 추가
JJ503 Oct 6, 2023
7d27cca
feat: 답변 신고 조회 서비스 추가 및 테스트 수정
JJ503 Oct 6, 2023
ef8a11d
feat: 답변 신고 조회 api 추가
JJ503 Oct 6, 2023
a929963
feat: flyway 스크립트 report 테이블 생성 쿼리 추가
JJ503 Oct 6, 2023
091bb9a
ci: 브랜치 최신화
JJ503 Oct 7, 2023
8d7c84a
ci: 충돌문제 해결
JJ503 Oct 7, 2023
3a20334
fix: 답변의 작성자 조회 시 질문된 경매의 판매자를 전달하도록 수정
JJ503 Oct 9, 2023
14f1477
test: 누락된 테스트 추가
JJ503 Oct 9, 2023
2370238
test: 누락된 테스트 추가
JJ503 Oct 9, 2023
c49fac1
refactor: import 와일드카드 제거
JJ503 Oct 9, 2023
e726017
refactor: 누락된 final 추가
JJ503 Oct 9, 2023
f35b69f
refactor: 컨벤션에 따른 개행 추가
JJ503 Oct 9, 2023
7ad8437
docs: 누락된 api 문서화 추가
JJ503 Oct 9, 2023
5285c0c
test: 누락된 테스트 추가
JJ503 Oct 9, 2023
e820761
fix: 신고 존재여부 조회 시 의도와 다른 로직 문제 해결
JJ503 Oct 9, 2023
b99d36c
test: 테스트 픽스처 수정
JJ503 Oct 9, 2023
624edc3
ci: 브랜치 최신화
JJ503 Oct 9, 2023
e79559b
test: 메서드 명 수정
JJ503 Oct 9, 2023
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
113 changes: 110 additions & 3 deletions backend/ddang/src/docs/asciidoc/docs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ include::{snippets}/user-controller-test/사용자_정보를_조회한다/respon

==== 요청

include::{snippets}/user-controller-test/사용자_정보를_수정한다/http-request.adoc[]
include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/http-request.adoc[]

==== 응답

include::{snippets}/user-controller-test/사용자_정보를_수정한다/http-response.adoc[]
include::{snippets}/user-controller-test/사용자_정보를_수정한다/response-fields.adoc[]
include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/http-response.adoc[]
include::{snippets}/user-controller-test/사용자_정보를_모두_수정한다/response-fields.adoc[]

== 카테고리 API

Expand Down Expand Up @@ -251,6 +251,67 @@ include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_

include::{snippets}/auction-controller-test/지정한_아이디에_해당하는_경매를_삭제한다/http-response.adoc[]

== Q&A API

=== 질문 등록

==== 요청

include::{snippets}/qna-controller-test/질문을_등록한다/http-request.adoc[]
include::{snippets}/qna-controller-test/질문을_등록한다/request-headers.adoc[]
include::{snippets}/qna-controller-test/질문을_등록한다/request-fields.adoc[]

==== 응답

include::{snippets}/qna-controller-test/질문을_등록한다/http-response.adoc[]

=== 질문 삭제

==== 요청

include::{snippets}/qna-controller-test/질문을_삭제한다/http-request.adoc[]
include::{snippets}/qna-controller-test/질문을_삭제한다/request-headers.adoc[]
include::{snippets}/qna-controller-test/질문을_삭제한다/path-parameters.adoc[]

==== 응답

include::{snippets}/qna-controller-test/질문을_삭제한다/http-response.adoc[]

=== 답변 등록

==== 요청

include::{snippets}/qna-controller-test/답변을_등록한다/http-request.adoc[]
include::{snippets}/qna-controller-test/답변을_등록한다/request-headers.adoc[]
include::{snippets}/qna-controller-test/답변을_등록한다/request-fields.adoc[]

==== 응답

include::{snippets}/qna-controller-test/답변을_등록한다/http-response.adoc[]

=== 답변 삭제

==== 요청

include::{snippets}/qna-controller-test/답변을_삭제한다/http-request.adoc[]
include::{snippets}/qna-controller-test/답변을_삭제한다/request-headers.adoc[]
include::{snippets}/qna-controller-test/답변을_삭제한다/path-parameters.adoc[]

==== 응답

include::{snippets}/qna-controller-test/답변을_삭제한다/http-response.adoc[]

=== Q&A 조회

==== 요청

include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/http-request.adoc[]

==== 응답

include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/http-response.adoc[]
include::{snippets}/auction-qna-controller-test/경매_아이디를_통해_질문과_답변을_모두_조회한다/response-fields.adoc[]

== 입찰 API

=== 입찰 등록
Expand Down Expand Up @@ -391,6 +452,52 @@ include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조
include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조회한다/http-response.adoc[]
include::{snippets}/report-controller-test/전체_채팅방_신고_목록을_조회한다/response-fields.adoc[]

=== 질문 신고 등록

==== 요청

include::{snippets}/report-controller-test/질문_신고를_등록한다/http-request.adoc[]
include::{snippets}/report-controller-test/질문_신고를_등록한다/request-headers.adoc[]
include::{snippets}/report-controller-test/질문_신고를_등록한다/request-fields.adoc[]

==== 응답

include::{snippets}/report-controller-test/질문_신고를_등록한다/http-response.adoc[]

=== 채팅방 신고 조회

==== 요청

include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/http-request.adoc[]

==== 응답

include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/http-response.adoc[]
include::{snippets}/report-controller-test/전체_질문_신고_목록을_조회한다/response-fields.adoc[]

=== 답변 신고 등록

==== 요청

include::{snippets}/report-controller-test/답변_신고를_등록한다/http-request.adoc[]
include::{snippets}/report-controller-test/답변_신고를_등록한다/request-headers.adoc[]
include::{snippets}/report-controller-test/답변_신고를_등록한다/request-fields.adoc[]

==== 응답

include::{snippets}/report-controller-test/답변_신고를_등록한다/http-response.adoc[]

=== 채팅방 신고 조회

==== 요청

include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/http-request.adoc[]

==== 응답

include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/http-response.adoc[]
include::{snippets}/report-controller-test/전체_답변_신고_목록을_조회한다/response-fields.adoc[]

== 디바이스 토큰 API

=== 디바이스 토큰 갱신
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
import com.ddang.ddang.auction.domain.Auction;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface JpaAuctionRepository extends JpaRepository<Auction, Long>, QuerydslAuctionRepository, QuerydslAuctionAndImageRepository {

Optional<Auction> findByIdAndDeletedIsFalse(final Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ddang.ddang.auction.presentation;

import com.ddang.ddang.auction.presentation.dto.response.ReadQnasResponse;
import com.ddang.ddang.qna.application.QuestionService;
import com.ddang.ddang.qna.application.dto.ReadQnasDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auctions")
@RequiredArgsConstructor
public class AuctionQnaController {

private final QuestionService questionService;

@GetMapping("/{auctionId}/questions")
public ResponseEntity<ReadQnasResponse> readAllByAuctionId(@PathVariable final Long auctionId) {
final ReadQnasDto readQnasDto = questionService.readAllByAuctionId(auctionId);
final ReadQnasResponse response = ReadQnasResponse.from(readQnasDto);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.qna.application.dto.ReadAnswerDto;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

public record ReadAnswerResponse(
Long id,

ReadUserInAuctionQuestionResponse writer,

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
LocalDateTime createdTime,

String content
) {

public static ReadAnswerResponse from(final ReadAnswerDto readAnswerDto) {
return new ReadAnswerResponse(
readAnswerDto.id(),
ReadUserInAuctionQuestionResponse.from(readAnswerDto.writerDto()),
readAnswerDto.createdTime(),
readAnswerDto.content()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.qna.application.dto.ReadAnswerDto;
import com.ddang.ddang.qna.application.dto.ReadQnaDto;

public record ReadQnaResponse(
ReadQuestionResponse question,
ReadAnswerResponse answer
) {

public static ReadQnaResponse from(final ReadQnaDto readQnaDto) {
final ReadQuestionResponse question = ReadQuestionResponse.from(readQnaDto.readQuestionDto());
final ReadAnswerResponse answer = processReadAnswerResponse(readQnaDto.readAnswerDto());

return new ReadQnaResponse(question, answer);
}

private static ReadAnswerResponse processReadAnswerResponse(final ReadAnswerDto readAnswerDto) {
if (readAnswerDto == null) {
return null;
}

return ReadAnswerResponse.from(readAnswerDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.qna.application.dto.ReadQnaDto;
import com.ddang.ddang.qna.application.dto.ReadQnasDto;

import java.util.List;

public record ReadQnasResponse(List<ReadQnaResponse> qnas) {

public static ReadQnasResponse from(final ReadQnasDto readQnasDto) {
final List<ReadQnaDto> dtos = readQnasDto.readQnaDtos();
final List<ReadQnaResponse> readQnaResponses = dtos.stream()
.map(ReadQnaResponse::from)
.toList();

return new ReadQnasResponse(readQnaResponses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.qna.application.dto.ReadQuestionDto;
import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

public record ReadQuestionResponse(
Long id,

ReadUserInAuctionQuestionResponse writer,

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
LocalDateTime createdTime,

String content
) {

public static ReadQuestionResponse from(final ReadQuestionDto readQuestionDto) {
return new ReadQuestionResponse(
readQuestionDto.id(),
ReadUserInAuctionQuestionResponse.from(readQuestionDto.readUserInQnaDto()),
readQuestionDto.createdTime(),
readQuestionDto.content()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.image.presentation.util.ImageRelativeUrl;
import com.ddang.ddang.image.presentation.util.ImageUrlCalculator;
import com.ddang.ddang.qna.application.dto.ReadUserInQnaDto;

public record ReadUserInAuctionQuestionResponse(Long id, String name, String image) {

public static ReadUserInAuctionQuestionResponse from(
final ReadUserInQnaDto writerDto
) {
return new ReadUserInAuctionQuestionResponse(
writerDto.id(),
writerDto.name(),
ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, writerDto.profileImageId())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@
import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException;
import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException;
import com.ddang.ddang.notification.application.exception.NotificationFailedException;
import com.ddang.ddang.qna.application.exception.AlreadyAnsweredException;
import com.ddang.ddang.qna.application.exception.AnswerNotFoundException;
import com.ddang.ddang.qna.application.exception.InvalidAnswererException;
import com.ddang.ddang.qna.application.exception.InvalidAuctionToAskQuestionException;
import com.ddang.ddang.qna.application.exception.InvalidQuestionerException;
import com.ddang.ddang.qna.application.exception.QuestionNotFoundException;
import com.ddang.ddang.region.application.exception.RegionNotFoundException;
import com.ddang.ddang.report.application.exception.AlreadyReportAuctionException;
import com.ddang.ddang.report.application.exception.AlreadyReportChatRoomException;
import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException;
import com.ddang.ddang.report.application.exception.InvalidQuestionReportException;
import com.ddang.ddang.report.application.exception.InvalidReportAuctionException;
import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException;
import com.ddang.ddang.review.application.exception.AlreadyReviewException;
Expand Down Expand Up @@ -346,6 +353,62 @@ public ResponseEntity<ExceptionResponse> handleReviewNotFoundException(
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidAuctionToAskQuestionException.class)
public ResponseEntity<ExceptionResponse> handleInvalidAuctionToAskQuestionException(final InvalidAuctionToAskQuestionException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidQuestionerException.class)
public ResponseEntity<ExceptionResponse> handleInvalidQuestionerException(final InvalidQuestionerException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(QuestionNotFoundException.class)
public ResponseEntity<ExceptionResponse> handleQuestionNotFoundException(final QuestionNotFoundException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(AnswerNotFoundException.class)
public ResponseEntity<ExceptionResponse> handleAnswerNotFoundException(final AnswerNotFoundException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidAnswererException.class)
public ResponseEntity<ExceptionResponse> handleInvalidAnswererException(final InvalidAnswererException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidQuestionReportException.class)
public ResponseEntity<ExceptionResponse> handleInvalidQuestionReportException(final InvalidQuestionReportException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(AlreadyAnsweredException.class)
public ResponseEntity<ExceptionResponse> handleAlreadyAnsweredException(final AlreadyAnsweredException ex) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
final MethodArgumentNotValidException ex,
Expand Down
Loading
Loading