diff --git a/backend/src/main/java/org/example/backend/domain/board/repository/ProductBoardRepository.java b/backend/src/main/java/org/example/backend/domain/board/repository/ProductBoardRepository.java index 755fcb98..1e001a01 100644 --- a/backend/src/main/java/org/example/backend/domain/board/repository/ProductBoardRepository.java +++ b/backend/src/main/java/org/example/backend/domain/board/repository/ProductBoardRepository.java @@ -1,5 +1,6 @@ package org.example.backend.domain.board.repository; +import java.util.List; import java.util.Optional; import org.example.backend.domain.board.model.entity.ProductBoard; @@ -13,6 +14,8 @@ public interface ProductBoardRepository extends JpaRepository findByIdx(Long idx); + List findByCompanyEmail(String companyEmail); + @Query("SELECT pb FROM ProductBoard pb JOIN FETCH pb.category WHERE pb.idx = :idx and pb.company.idx = :companyIdx") Optional findByCompanyIdxAndIdx(Long companyIdx, Long idx); diff --git a/backend/src/main/java/org/example/backend/domain/qna/controller/AnswerController.java b/backend/src/main/java/org/example/backend/domain/qna/controller/AnswerController.java index f4379f0d..fd48e3f0 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/controller/AnswerController.java +++ b/backend/src/main/java/org/example/backend/domain/qna/controller/AnswerController.java @@ -1,4 +1,50 @@ package org.example.backend.domain.qna.controller; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.qna.model.dto.AnswerDto; +import org.example.backend.domain.qna.service.AnswerService; +import org.example.backend.global.common.constants.BaseResponse; +import org.example.backend.global.common.constants.BaseResponseStatus; +import org.example.backend.global.exception.InvalidCustomException; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/qna/answer") +@RequiredArgsConstructor public class AnswerController { + + private final AnswerService answerService; + + @Operation(summary = "답변 등록 API", description = "기업회원이 문의에 대한 답변을 등록합니다.") + @PostMapping("/create") + public BaseResponse createAnswer(@RequestBody AnswerDto.AnswerCreateRequest request, @AuthenticationPrincipal UserDetails userDetails) { + try { + String email = userDetails.getUsername(); // 인증된 기업회원 이메일 + + // 답변 등록 서비스 호출 + answerService.createAnswer(request, email); + return new BaseResponse<>(BaseResponseStatus.SUCCESS); + } catch (InvalidCustomException e) { + return new BaseResponse<>(e.getStatus()); + } catch (Exception e) { + return new BaseResponse<>(BaseResponseStatus.FAIL); + } + } + + @Operation(summary = "답변 삭제 API", description = "답변을 삭제합니다.") + @DeleteMapping("/delete/{id}") + public BaseResponse deleteAnswer(@PathVariable Long id, @AuthenticationPrincipal UserDetails userDetails) { + try { + String email = userDetails.getUsername(); // 인증된 기업회원 이메일 + answerService.deleteAnswer(id, email); + return new BaseResponse<>(BaseResponseStatus.SUCCESS); + } catch (InvalidCustomException e) { + return new BaseResponse<>(e.getStatus()); + } catch (Exception e) { + return new BaseResponse<>(BaseResponseStatus.FAIL); + } + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/controller/QuestionController.java b/backend/src/main/java/org/example/backend/domain/qna/controller/QuestionController.java index 328703b9..929fad1f 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/controller/QuestionController.java +++ b/backend/src/main/java/org/example/backend/domain/qna/controller/QuestionController.java @@ -34,20 +34,11 @@ public class QuestionController { ) )) @PostMapping("/create") - public BaseResponse create(@RequestBody QuestionDto.QuestionCreateRequest request, - @AuthenticationPrincipal UserDetails userDetails) { + public BaseResponse create(@RequestBody QuestionDto.QuestionCreateRequest request, @AuthenticationPrincipal UserDetails userDetails) { String email = userDetails.getUsername(); // 인증된 사용자의 이메일 가져오기 - - if (request.getTitle() == null || request.getTitle().trim().isEmpty()) { - throw new InvalidCustomException(BaseResponseStatus.QNA_QUESTION_FAIL_EMPTY_TITLE); - } - if (request.getContent() == null || request.getContent().trim().isEmpty()) { - throw new InvalidCustomException(BaseResponseStatus.QNA_QUESTION_FAIL_EMPTY_CONTENT); - } - Long productBoardIdx = request.getProductBoardIdx(); // Request Body로 전달받은 productBoardIdx 사용 - QuestionDto.QuestionCreateResponse response = questionService.createQuestion(request, email, productBoardIdx); + QuestionDto.QuestionCreateResponse response = questionService.createQuestion(request, email, productBoardIdx); return new BaseResponse<>(response); } @@ -59,10 +50,17 @@ public BaseResponse> getQuestions() { } @DeleteMapping("/delete/{id}") - public BaseResponse deleteQuestion(@PathVariable Long id, - @AuthenticationPrincipal UserDetails userDetails) { + public BaseResponse deleteQuestion(@PathVariable Long id, @AuthenticationPrincipal UserDetails userDetails) { String email = userDetails.getUsername(); questionService.deleteQuestion(id, email); return new BaseResponse<>(BaseResponseStatus.SUCCESS); } + + @Operation(summary = "문의 목록 조회 API", description = "로그인된 기업 회원이 자신의 게시글에 달린 문의만 조회합니다.") + @GetMapping("/list/company") + public BaseResponse> getCompanyQuestions(@AuthenticationPrincipal UserDetails userDetails) { + String companyEmail = userDetails.getUsername(); // 인증된 기업 회원의 이메일 가져오기 + List questionList = questionService.getQuestionsByCompanyEmail(companyEmail); + return new BaseResponse<>(questionList); + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/model/dto/AnswerDto.java b/backend/src/main/java/org/example/backend/domain/qna/model/dto/AnswerDto.java index 413824a7..845cae9c 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/model/dto/AnswerDto.java +++ b/backend/src/main/java/org/example/backend/domain/qna/model/dto/AnswerDto.java @@ -1,4 +1,42 @@ package org.example.backend.domain.qna.model.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.backend.domain.company.model.entity.Company; +import org.example.backend.domain.qna.model.entity.Answer; +import org.example.backend.domain.qna.model.entity.Question; + +import java.time.LocalDateTime; + public class AnswerDto { + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AnswerCreateRequest { + private Long questionIdx; + private String content; + + public Answer toEntity(Company company, Question question) { + return Answer.builder() + .content(this.content) + .company(company) + .question(question) + .createdAt(LocalDateTime.now()) + .build(); + } + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AnswerResponse { + private Long idx; + private String content; + private String companyName; + private LocalDateTime createdAt; + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/model/dto/QuestionDto.java b/backend/src/main/java/org/example/backend/domain/qna/model/dto/QuestionDto.java index 8d50c324..76bc1f47 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/model/dto/QuestionDto.java +++ b/backend/src/main/java/org/example/backend/domain/qna/model/dto/QuestionDto.java @@ -12,6 +12,7 @@ import org.example.backend.global.common.constants.AnswerStatus; import java.time.LocalDateTime; +import java.util.List; public class QuestionDto { @@ -50,6 +51,9 @@ public static class QuestionCreateResponse{ private String userName; private String answerStatus; private LocalDateTime createdAt; + + private String answerContent; + private LocalDateTime answerCreatedAt; } @Getter @@ -64,5 +68,7 @@ public static class QuestionListResponse { private String answerStatus; private LocalDateTime createdAt; private String email; + private Long productBoardIdx; + private List answers; } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/model/entity/Answer.java b/backend/src/main/java/org/example/backend/domain/qna/model/entity/Answer.java index de6bf97c..c2882637 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/model/entity/Answer.java +++ b/backend/src/main/java/org/example/backend/domain/qna/model/entity/Answer.java @@ -1,4 +1,45 @@ package org.example.backend.domain.qna.model.entity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.example.backend.domain.company.model.entity.Company; +import org.example.backend.domain.qna.model.dto.AnswerDto; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Answer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idx; + + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "company_idx") + private Company company; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "question_idx") + private Question question; + + private LocalDateTime createdAt; + + // DTO 변환 메서드: 답변 조회 응답 + public AnswerDto.AnswerResponse toResponse() { + return AnswerDto.AnswerResponse.builder() + .idx(this.idx) + .content(this.content) + .companyName(this.company.getName()) + .createdAt(this.createdAt) + .build(); + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/model/entity/Question.java b/backend/src/main/java/org/example/backend/domain/qna/model/entity/Question.java index bb9383e2..99eb5dc2 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/model/entity/Question.java +++ b/backend/src/main/java/org/example/backend/domain/qna/model/entity/Question.java @@ -1,19 +1,19 @@ package org.example.backend.domain.qna.model.entity; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.example.backend.domain.board.model.entity.ProductBoard; +import org.example.backend.domain.qna.model.dto.AnswerDto; import org.example.backend.domain.qna.model.dto.QuestionDto; import org.example.backend.domain.user.model.entity.User; import org.example.backend.global.common.constants.AnswerStatus; import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Entity @Getter @@ -38,43 +38,63 @@ public class Question { @JoinColumn(name = "product_board_idx") private ProductBoard productBoard; + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) + private List answers = new ArrayList<>(); + @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; - @LastModifiedDate @Column(insertable = false) private LocalDateTime modifiedAt; @Column(name = "answer_status") private String answerStatus = AnswerStatus.ANSWER_WAITING.getStatus(); - public QuestionDto.QuestionListResponse toQuestionListResponse() { - return QuestionDto.QuestionListResponse.builder() + // DTO 변환 메서드: 문의 생성 후 응답 + public QuestionDto.QuestionCreateResponse toCreateResponse() { + return QuestionDto.QuestionCreateResponse.builder() .idx(this.idx) .title(this.title) .content(this.content) .userName(this.user.getName()) .answerStatus(this.answerStatus) .createdAt(this.createdAt) - .email(this.user.getEmail()) .build(); } - // 새로운 변환 메서드: 문의 생성 응답 DTO로 변환 - public QuestionDto.QuestionCreateResponse toQuestionCreateResponse() { - return QuestionDto.QuestionCreateResponse.builder() + // DTO 변환 메서드: 문의 목록 조회 + public QuestionDto.QuestionListResponse toListResponse() { + List answerResponses = this.answers.stream() + .map(answer -> answer.toResponse()) + .collect(Collectors.toList()); + + return QuestionDto.QuestionListResponse.builder() .idx(this.idx) .title(this.title) .content(this.content) .userName(this.user.getName()) .answerStatus(this.answerStatus) .createdAt(this.createdAt) + .email(this.user.getEmail()) + .answers(answerResponses) // 답변 리스트 포함 + .productBoardIdx(getProductBoard().getIdx()) .build(); } - // 답변이 등록되면 문의 상태를 "답변완료"로 변경하는 메서드 -// public void markAsAnswered(){ -// this.answerStatus = AnswerStatus.ANSWER_COMPLETED.getStatus(); -// } + // 답변이 등록 되면 문의 상태를 "답변완료"로 변경하는 메서드 + public void markAsAnswered(){ + this.answerStatus = AnswerStatus.ANSWER_COMPLETED.getStatus(); + } + + // 답변 상태를 "답변 대기"로 변경하는 메서드 + public void markAsWaiting() { + this.answerStatus = AnswerStatus.ANSWER_WAITING.getStatus(); + } + + // 문의 수정 시에만 수정 시간(modifiedAt) 갱신 + public void updateContent(String newContent) { + this.content = newContent; + this.modifiedAt = LocalDateTime.now(); // 문의가 수정될 때만 modifiedAt 갱신 + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/repository/AnswerRepository.java b/backend/src/main/java/org/example/backend/domain/qna/repository/AnswerRepository.java index f85f87fe..fa525dd8 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/repository/AnswerRepository.java +++ b/backend/src/main/java/org/example/backend/domain/qna/repository/AnswerRepository.java @@ -1,4 +1,11 @@ package org.example.backend.domain.qna.repository; -public interface AnswerRepository { +import org.example.backend.domain.qna.model.entity.Answer; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface AnswerRepository extends JpaRepository { + // 특정 문의에 달린 모든 답변을 리스트로 반환 + List findAllByQuestionIdx(Long questionIdx); } diff --git a/backend/src/main/java/org/example/backend/domain/qna/repository/QuestionRepository.java b/backend/src/main/java/org/example/backend/domain/qna/repository/QuestionRepository.java index 8252cb0e..01fd2d90 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/repository/QuestionRepository.java +++ b/backend/src/main/java/org/example/backend/domain/qna/repository/QuestionRepository.java @@ -1,9 +1,11 @@ package org.example.backend.domain.qna.repository; +import org.example.backend.domain.board.model.entity.ProductBoard; import org.example.backend.domain.qna.model.entity.Question; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface QuestionRepository extends JpaRepository { + List findByProductBoardIn(List productBoards); } diff --git a/backend/src/main/java/org/example/backend/domain/qna/service/AnswerService.java b/backend/src/main/java/org/example/backend/domain/qna/service/AnswerService.java index 897165c8..5f320fda 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/service/AnswerService.java +++ b/backend/src/main/java/org/example/backend/domain/qna/service/AnswerService.java @@ -1,4 +1,53 @@ package org.example.backend.domain.qna.service; +import lombok.RequiredArgsConstructor; +import org.example.backend.domain.board.model.entity.ProductBoard; +import org.example.backend.domain.qna.model.dto.AnswerDto; +import org.example.backend.domain.qna.model.entity.Answer; +import org.example.backend.domain.qna.model.entity.Question; +import org.example.backend.domain.qna.repository.AnswerRepository; +import org.example.backend.domain.qna.repository.QuestionRepository; +import org.example.backend.domain.company.model.entity.Company; +import org.example.backend.domain.company.repository.CompanyRepository; +import org.example.backend.global.common.constants.AnswerStatus; +import org.example.backend.global.common.constants.BaseResponseStatus; +import org.example.backend.global.exception.InvalidCustomException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor public class AnswerService { + + private final AnswerRepository answerRepository; + private final QuestionRepository questionRepository; + private final CompanyRepository companyRepository; + + public void createAnswer(AnswerDto.AnswerCreateRequest request, String email) { + Company company = companyRepository.findByEmail(email) + .orElseThrow(() -> new InvalidCustomException(BaseResponseStatus.QNA_ANSWERS_FAIL)); + + Question question = questionRepository.findById(request.getQuestionIdx()) + .orElseThrow(() -> new InvalidCustomException(BaseResponseStatus.QNA_ANSWER_FAIL_NOT_FOUND)); + + if (!question.getProductBoard().getCompany().getEmail().equals(company.getEmail())) { + throw new InvalidCustomException(BaseResponseStatus.FAIL_UNAUTHORIZED); + } + + Answer answer = request.toEntity(company, question); + answerRepository.save(answer); + + question.markAsAnswered(); + questionRepository.save(question); + } + + public void deleteAnswer(Long answerIdx, String email) { + Answer answer = answerRepository.findById(answerIdx) + .orElseThrow(() -> new InvalidCustomException(BaseResponseStatus.QNA_ANSWER_FAIL_NOT_FOUND)); + + if (!answer.getCompany().getEmail().equals(email)) { + throw new InvalidCustomException(BaseResponseStatus.FAIL_UNAUTHORIZED); + } + + answerRepository.delete(answer); + } } diff --git a/backend/src/main/java/org/example/backend/domain/qna/service/QuestionService.java b/backend/src/main/java/org/example/backend/domain/qna/service/QuestionService.java index 95721d4f..99bf7ad1 100644 --- a/backend/src/main/java/org/example/backend/domain/qna/service/QuestionService.java +++ b/backend/src/main/java/org/example/backend/domain/qna/service/QuestionService.java @@ -3,8 +3,11 @@ import lombok.RequiredArgsConstructor; import org.example.backend.domain.board.model.entity.ProductBoard; import org.example.backend.domain.board.repository.ProductBoardRepository; +import org.example.backend.domain.qna.model.dto.AnswerDto; import org.example.backend.domain.qna.model.dto.QuestionDto; +import org.example.backend.domain.qna.model.entity.Answer; import org.example.backend.domain.qna.model.entity.Question; +import org.example.backend.domain.qna.repository.AnswerRepository; import org.example.backend.domain.qna.repository.QuestionRepository; import org.example.backend.domain.user.model.entity.User; import org.example.backend.domain.user.repository.UserRepository; @@ -23,6 +26,7 @@ public class QuestionService { private final QuestionRepository questionRepository; private final ProductBoardRepository productBoardRepository; private final UserRepository userRepository; + private final AnswerRepository answerRepository; public QuestionDto.QuestionCreateResponse createQuestion(QuestionDto.QuestionCreateRequest request, String email, Long productBoardIdx) { // 사용자 조회 @@ -37,17 +41,16 @@ public QuestionDto.QuestionCreateResponse createQuestion(QuestionDto.QuestionCre Question question = request.toEntity(user, productBoard); questionRepository.save(question); - // 엔티티의 변환 메서드를 호출하여 DTO 반환 - return question.toQuestionCreateResponse(); + return question.toCreateResponse(); // 엔티티의 변환 메서드 사용 } public List getQuestions() { return questionRepository.findAll().stream() - .map(Question::toQuestionListResponse) // 엔티티의 변환 메서드 사용 + .map(Question::toListResponse) // 엔티티의 변환 메서드 사용 .collect(Collectors.toList()); } - public void deleteQuestion(Long questionId, String email) { + public void deleteQuestion(Long questionId, String email){ Question question = questionRepository.findById(questionId) .orElseThrow(() -> new InvalidCustomException(BaseResponseStatus.QNA_ANSWER_DELETE_FAIL_NOT_FOUND)); @@ -64,4 +67,12 @@ public void deleteQuestion(Long questionId, String email) { // 삭제 처리 questionRepository.delete(question); } + + public List getQuestionsByCompanyEmail(String companyEmail) { + List productBoards = productBoardRepository.findByCompanyEmail(companyEmail); + + return questionRepository.findByProductBoardIn(productBoards).stream() + .map(Question::toListResponse) // 엔티티의 변환 메서드 사용 + .collect(Collectors.toList()); + } }