Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ dependencies {
implementation 'org.postgresql:postgresql'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly "io.netty:netty-resolver-dns-native-macos:4.1.110.Final:osx-aarch_64"
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableAsync
@EnableScheduling
public class InsightPrepApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

Expand All @@ -29,7 +28,6 @@ public class CompanyAnalysisSection extends BaseTimeEntity {
@Column(nullable = false)
private String sectionTitle; // ex) 3줄 요약

@Lob
@Column(nullable = false)
@Column(columnDefinition = "TEXT", nullable = false)
private String content; // JSON 문자열 or 텍스트
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.project.InsightPrep.domain.question.controller;

import com.project.InsightPrep.domain.question.controller.docs.QuestionControllerDocs;
import com.project.InsightPrep.domain.question.dto.request.AnswerRequest;
import com.project.InsightPrep.domain.question.dto.response.AnswerResponse.AnswerDto;
import com.project.InsightPrep.domain.question.dto.response.AnswerResponse.FeedbackDto;
import com.project.InsightPrep.domain.question.dto.response.QuestionResponse;
import com.project.InsightPrep.domain.question.service.AnswerService;
import com.project.InsightPrep.domain.question.service.FeedbackService;
import com.project.InsightPrep.domain.question.service.QuestionService;
import com.project.InsightPrep.global.common.response.ApiResponse;
import com.project.InsightPrep.global.common.response.code.ApiSuccessCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/question")
@RequiredArgsConstructor
public class QuestionController implements QuestionControllerDocs {

private final QuestionService questionService;
private final AnswerService answerService;
private final FeedbackService feedbackService;

@Override
@PostMapping("/{category}")
@PreAuthorize("hasAnyRole('USER')")
public ResponseEntity<ApiResponse<QuestionResponse.QuestionDto>> createQuestion(@PathVariable @Valid String category) {
QuestionResponse.QuestionDto dto = questionService.createQuestion(category);
return ResponseEntity.ok(ApiResponse.of(ApiSuccessCode.CREATE_QUESTION_SUCCESS, dto));
}

@Override
@PostMapping("/{questionId}/answer")
@PreAuthorize("hasAnyRole('USER')")
public ResponseEntity<ApiResponse<AnswerDto>> saveAnswer(@RequestBody @Valid AnswerRequest.AnswerDto dto, @PathVariable Long questionId) {
answerService.saveAnswer(dto, questionId);
return ResponseEntity.ok(ApiResponse.of(ApiSuccessCode.SAVE_ANSWER_SUCCESS));
}

@Override
@PostMapping("/{answerId}/feedback")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public ResponseEntity<ApiResponse<FeedbackDto>> getFeedback(@PathVariable long answerId) {
FeedbackDto feedback = feedbackService.getFeedback(answerId);
if (feedback == null) {
return ResponseEntity.ok(ApiResponse.of(ApiSuccessCode.FEEDBACK_PENDING));
}
return ResponseEntity.ok(ApiResponse.of(ApiSuccessCode.GET_FEEDBACK_SUCCESS, feedback));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.project.InsightPrep.domain.question.controller.docs;

import com.project.InsightPrep.domain.question.dto.request.AnswerRequest;
import com.project.InsightPrep.domain.question.dto.response.AnswerResponse;
import com.project.InsightPrep.domain.question.dto.response.AnswerResponse.AnswerDto;
import com.project.InsightPrep.domain.question.dto.response.QuestionResponse;
import com.project.InsightPrep.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "Question", description = "Question 관련 API")
public interface QuestionControllerDocs {

@Operation(summary = "질문 생성", description = "로그인한 사용자는 카테고리를 선택한 후 질문 생성을 합니다. 생성된 질문은 답변을 할 시 db에 저장이 되며 이후에 조회가 가능합니다.")
public ResponseEntity<ApiResponse<QuestionResponse.QuestionDto>> createQuestion(@PathVariable String category);

@Operation(summary = "답변 작성", description = "질문에 대한 답변을 작성합니다.")
public ResponseEntity<ApiResponse<AnswerDto>> saveAnswer(@RequestBody @Valid AnswerRequest.AnswerDto dto, @PathVariable Long questionId);

@Operation(summary = "피드백 조회", description = "작성한 답변에 대한 피드백을 조회합니다. 폴링 방식을 적용하여 프론트엔드에서 일정 시간(3초)마다 해당 요청을 하고, 피드백이 생성되었으면 반환, 아니면 202(PENDING)으로 반복하도록 구성합니다.")
public ResponseEntity<ApiResponse<AnswerResponse.FeedbackDto>> getFeedback(@PathVariable long answerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.project.InsightPrep.domain.question.dto.request;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
public class AnswerRequest {

@Builder
@Getter
@Setter
@JsonInclude(Include.NON_NULL)
@NoArgsConstructor
@AllArgsConstructor
public static class AnswerDto {

@NotBlank(message = "답변을 작성해주세요.")
private String content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.project.InsightPrep.domain.question.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Builder;
import lombok.Getter;

public class AnswerResponse {

@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class AnswerDto {

private long id;
private String content;
}

@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class FeedbackDto {

private long feedbackId;
private long questionId;
private long answerId;

private int score;
private String improvement; // 요약 및 정리
private String modelAnswer; // 개선점 제안
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.project.InsightPrep.domain.question.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public class FeedbackResponse {
private int score;
private String improvement;
private String modelAnswer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.project.InsightPrep.domain.question.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.project.InsightPrep.domain.question.entity.AnswerStatus;
import lombok.Builder;
import lombok.Getter;

public class QuestionResponse {

@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class QuestionDto {

private long id;
private String content;
private String category;
private AnswerStatus status;
}

@Getter
@Builder
@JsonInclude(Include.NON_NULL)
public static class GptQuestion {
private String question;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

import com.project.InsightPrep.domain.member.entity.Member;
import com.project.InsightPrep.global.common.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "answer")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Answer extends BaseTimeEntity {

@Id
Expand All @@ -27,6 +36,6 @@ public class Answer extends BaseTimeEntity {
@JoinColumn(name = "question_id", nullable = false)
private Question question;

@Lob
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package com.project.InsightPrep.domain.question.entity;

import com.project.InsightPrep.global.common.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "answer_feedback")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class AnswerFeedback extends BaseTimeEntity {

@Id
Expand All @@ -24,9 +33,9 @@ public class AnswerFeedback extends BaseTimeEntity {

private Integer score;

@Lob
private String summary; // 간단한 요약

@Lob
@Column(columnDefinition = "TEXT", nullable = false)
private String improvement; // 개선점 제안

@Column(columnDefinition = "TEXT", nullable = false)
private String modelAnswer; // 면접관의 정답
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.project.InsightPrep.domain.question.entity;

public enum AnswerStatus {
WAITING, ANSWERED
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@
import com.project.InsightPrep.global.common.entity.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;

import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "question")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Question extends BaseTimeEntity {

@Id
Expand All @@ -23,6 +33,10 @@ public class Question extends BaseTimeEntity {
@Column(nullable = false)
private String category; // OS, DB 등

@Lob
@Column(columnDefinition = "TEXT", nullable = false)
private String content;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AnswerStatus status = AnswerStatus.WAITING;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.project.InsightPrep.domain.question.mapper;

import com.project.InsightPrep.domain.question.entity.Answer;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AnswerMapper {
void insertAnswer(Answer answer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.project.InsightPrep.domain.question.mapper;

import com.project.InsightPrep.domain.question.entity.AnswerFeedback;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface FeedbackMapper {

void insertFeedback(AnswerFeedback feedback);

AnswerFeedback findById(@Param("answerId") long answerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.project.InsightPrep.domain.question.mapper;

import com.project.InsightPrep.domain.question.entity.Question;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface QuestionMapper {
void insertQuestion(Question question);

Question findById(@Param("id") Long id);

void updateStatus(@Param("questionId") Long questionId, @Param("status") String answerStatus);

void deleteUnansweredQuestions(@Param("status") String answerStatus);
}
Loading