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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
import com.example.trouble_log.domain.interview.dto.ReportResponse;
import com.example.trouble_log.domain.interview.service.InterviewService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -19,20 +24,46 @@ public class InterviewController {

private final InterviewService interviewService;

@Operation(summary = "답변 제출", description = "면접 질문에 대한 답변을 저장하고 AI 피드백을 반환합니다.")
@Operation(
summary = "면접 질문 답변 저장 및 AI 피드백 생성",
description = "특정 프로젝트 세션의 면접 질문에 대한 답변을 저장합니다. 답변이 비어 있으면 스킵으로 처리하고, 답변이 있으면 Azure OpenAI 피드백을 생성해 개선 제안과 주의 메시지를 반환합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "답변 저장 성공",
content = @Content(schema = @Schema(implementation = AnswerResponse.class))),
@ApiResponse(responseCode = "400", description = "요청 본문 누락"),
@ApiResponse(responseCode = "403", description = "해당 세션의 질문이 아님"),
@ApiResponse(responseCode = "404", description = "세션, 질문 또는 사전 컨텍스트를 찾을 수 없음")
})
@PostMapping("/{sessionId}/answers/{questionId}")
public ResponseEntity<AnswerResponse> saveAnswer(
@Parameter(description = "프로젝트 세션 ID", example = "1", required = true)
@PathVariable Long sessionId,
@Parameter(description = "답변을 저장할 면접 질문 ID", example = "1", required = true)
@PathVariable Long questionId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원 ID와 답변 내용. 답변이 null 또는 공백이면 스킵으로 저장됩니다.",
required = true,
content = @Content(schema = @Schema(implementation = AnswerRequest.class))
)
@RequestBody AnswerRequest request
) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(interviewService.saveAnswer(sessionId, questionId, request));
}

@Operation(summary = "리포트 생성", description = "저장된 Q&A를 바탕으로 트러블슈팅 리포트를 생성합니다.")
@Operation(
summary = "트러블슈팅 리포트 생성",
description = "저장된 면접 질문과 답변, 사전 컨텍스트, 코드 평가 결과를 바탕으로 마크다운 리포트와 역량 레이더 점수를 생성합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "리포트 생성 성공",
content = @Content(schema = @Schema(implementation = ReportResponse.class))),
@ApiResponse(responseCode = "404", description = "세션 또는 사전 컨텍스트를 찾을 수 없음")
})
@PostMapping("/{sessionId}/report")
public ResponseEntity<ReportResponse> generateReport(
@Parameter(description = "프로젝트 세션 ID", example = "1", required = true)
@PathVariable Long sessionId
) {
return ResponseEntity.ok(interviewService.generateReport(sessionId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
import com.example.trouble_log.domain.interview.dto.InterviewSubmitRequest;
import com.example.trouble_log.domain.interview.dto.InterviewSubmitResponse;
import com.example.trouble_log.domain.interview.service.InterviewSubmissionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -14,13 +21,31 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/projects")
@Tag(name = "Interview Submission", description = "면접 답변 최종 제출 및 개인정보 감지 API")
public class InterviewSubmissionController {

private final InterviewSubmissionService interviewSubmissionService;

@Operation(
summary = "면접 답변 최종 제출",
description = "프로젝트 세션에 생성된 면접 질문 3개에 대한 최종 답변을 저장합니다. 답변 내 개인정보가 감지되면 제출을 차단하고 감지 유형과 경고 메시지를 반환합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "최종 제출 성공",
content = @Content(schema = @Schema(implementation = InterviewSubmitResponse.class))),
@ApiResponse(responseCode = "400", description = "필수 값 누락, 질문 3개 미준비, 답변 개수 불일치, 개인정보 감지"),
@ApiResponse(responseCode = "403", description = "해당 프로젝트 세션에 접근할 수 없음"),
@ApiResponse(responseCode = "404", description = "프로젝트 세션을 찾을 수 없음")
})
@PostMapping("/{sessionId}/interview/submit")
public ResponseEntity<InterviewSubmitResponse> submit(
@Parameter(description = "프로젝트 세션 ID", example = "1", required = true)
@PathVariable Long sessionId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원 ID와 질문별 최종 답변 3개",
required = true,
content = @Content(schema = @Schema(implementation = InterviewSubmitRequest.class))
)
@RequestBody InterviewSubmitRequest request
) {
InterviewSubmitResponse response = interviewSubmissionService.submit(sessionId, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ public InterviewAnswer(InterviewQuestion interviewQuestion, String answer) {
this(interviewQuestion, answer, false, null); // 생성자 2 호출
}

public void updateAnswer(String answer) {
public void update(String answer, boolean isSkipped, String feedback) {
this.answer = answer;
this.isSkipped = isSkipped;
this.feedback = feedback;
}

public void updateAnswer(String answer) {
update(answer, answer == null || answer.isBlank(), null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public AnswerResponse saveAnswer(Long sessionId, Long questionId, AnswerRequest
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "사전 컨텍스트를 찾을 수 없습니다."));

if (request == null) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "답변 요청 본문은 필수입니다.");
}

// 스킵 여부 판단
boolean isSkipped = request.getAnswer() == null || request.getAnswer().isBlank();

Expand All @@ -90,8 +95,9 @@ public AnswerResponse saveAnswer(Long sessionId, Long questionId, AnswerRequest
warning = feedbackResult.getWarning();
}

InterviewAnswer interviewAnswer = new InterviewAnswer(
question,
InterviewAnswer interviewAnswer = answerRepository.findByInterviewQuestionId(questionId)
.orElseGet(() -> new InterviewAnswer(question, null, true, null));
interviewAnswer.update(
isSkipped ? null : request.getAnswer(),
isSkipped,
feedbackJson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
import com.example.trouble_log.domain.projectSession.dto.ProjectSessionRequest;
import com.example.trouble_log.domain.projectSession.dto.ProjectSessionResponse;
import com.example.trouble_log.domain.projectSession.service.ProjectSessionService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -17,19 +24,55 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/projects")
@Tag(name = "Project Session", description = "프로젝트 코드 입력, 사전 컨텍스트 저장, AI 면접 질문 생성 API")
public class ProjectSessionController {

private final ProjectSessionService projectSessionService;

@Operation(
summary = "프로젝트 세션 생성",
description = "회원이 입력한 소스코드와 선택적인 GitHub URL을 저장하고, 이후 사전 컨텍스트와 면접 질문 생성에 사용할 세션 ID를 반환합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "프로젝트 세션 생성 성공",
content = @Content(schema = @Schema(implementation = ProjectSessionResponse.class))),
@ApiResponse(responseCode = "400", description = "회원 ID 또는 소스코드 누락"),
@ApiResponse(responseCode = "404", description = "회원을 찾을 수 없음")
})
@PostMapping()
public ResponseEntity<ProjectSessionResponse> create(@RequestBody ProjectSessionRequest request) {
public ResponseEntity<ProjectSessionResponse> create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "프로젝트 세션 생성을 위한 회원 ID, 소스코드, GitHub URL",
required = true,
content = @Content(schema = @Schema(implementation = ProjectSessionRequest.class))
)
@RequestBody ProjectSessionRequest request
) {
ProjectSessionResponse response = projectSessionService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@Operation(
summary = "사전 컨텍스트 저장 및 면접 질문 생성",
description = "프로젝트 목적, 기술 선택 이유, 예외 처리 방식, 프로젝트 규모를 저장한 뒤 Azure OpenAI로 면접 질문 3개를 생성합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "사전 컨텍스트 저장 및 질문 생성 성공",
content = @Content(schema = @Schema(implementation = PreContextResponse.class))),
@ApiResponse(responseCode = "400", description = "필수 사전 컨텍스트 값 누락"),
@ApiResponse(responseCode = "403", description = "해당 프로젝트 세션에 접근할 수 없음"),
@ApiResponse(responseCode = "404", description = "프로젝트 세션을 찾을 수 없음"),
@ApiResponse(responseCode = "409", description = "이미 사전 컨텍스트가 저장된 세션")
})
@PostMapping("/{sessionId}/pre-context")
public ResponseEntity<PreContextResponse> createPreContext(
@Parameter(description = "프로젝트 세션 ID", example = "1", required = true)
@PathVariable Long sessionId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "면접 질문 생성을 위한 사전 컨텍스트",
required = true,
content = @Content(schema = @Schema(implementation = PreContextRequest.class))
)
@RequestBody PreContextRequest request
) {
PreContextResponse response = projectSessionService.createPreContext(sessionId, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import com.example.trouble_log.domain.user.dto.LoginResponse;
import com.example.trouble_log.domain.user.dto.SignUpRequest;
import com.example.trouble_log.domain.user.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -15,24 +21,64 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
@Tag(name = "Member", description = "회원가입, 로그인, 로그아웃 API")
public class MemberController {

private final MemberService memberService;

@Operation(
summary = "로그인",
description = "이메일과 비밀번호를 검증하고, 프론트에서 이후 API 호출에 사용할 회원 ID를 반환합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "로그인 성공",
content = @Content(schema = @Schema(implementation = LoginResponse.class))),
@ApiResponse(responseCode = "400", description = "이메일 또는 비밀번호 누락"),
@ApiResponse(responseCode = "401", description = "이메일 또는 비밀번호 불일치")
})
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
public ResponseEntity<LoginResponse> login(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인에 사용할 이메일과 비밀번호",
required = true,
content = @Content(schema = @Schema(implementation = LoginRequest.class))
)
@RequestBody LoginRequest request
) {
LoginResponse response = memberService.login(request);
return ResponseEntity.ok(response);
}

@Operation(
summary = "로그아웃",
description = "현재는 서버 세션이나 토큰을 무효화하지 않고, 프론트 로그아웃 플로우를 위한 종료 응답만 반환합니다."
)
@ApiResponse(responseCode = "204", description = "로그아웃 처리 완료")
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
memberService.logout();
return ResponseEntity.noContent().build();
}

@Operation(
summary = "회원가입",
description = "신규 회원을 생성하고, 생성된 회원 정보를 로그인 응답 형태로 반환합니다."
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "회원가입 성공",
content = @Content(schema = @Schema(implementation = LoginResponse.class))),
@ApiResponse(responseCode = "400", description = "이메일, 비밀번호 또는 사용자 이름 누락"),
@ApiResponse(responseCode = "409", description = "이미 사용 중인 이메일")
})
@PostMapping("/signup")
public ResponseEntity<LoginResponse> signup(@RequestBody SignUpRequest request) {
public ResponseEntity<LoginResponse> signup(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원가입에 필요한 이메일, 비밀번호, 사용자 이름",
required = true,
content = @Content(schema = @Schema(implementation = SignUpRequest.class))
)
@RequestBody SignUpRequest request
) {
LoginResponse response = memberService.signup(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Expand Down
Loading
Loading