diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/config/AsyncConfig.java b/src/main/java/io/wisoft/prepair/prepair_api/common/config/AsyncConfig.java similarity index 92% rename from src/main/java/io/wisoft/prepair/prepair_api/global/config/AsyncConfig.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/config/AsyncConfig.java index a2363fa..b094e5f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/config/AsyncConfig.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/config/AsyncConfig.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.config; +package io.wisoft.prepair.prepair_api.common.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/config/JacksonConfig.java b/src/main/java/io/wisoft/prepair/prepair_api/common/config/JacksonConfig.java similarity index 85% rename from src/main/java/io/wisoft/prepair/prepair_api/global/config/JacksonConfig.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/config/JacksonConfig.java index edd3a94..e464f7b 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/config/JacksonConfig.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/config/JacksonConfig.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.config; +package io.wisoft.prepair.prepair_api.common.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/config/RestClientConfig.java b/src/main/java/io/wisoft/prepair/prepair_api/common/config/RestClientConfig.java similarity index 85% rename from src/main/java/io/wisoft/prepair/prepair_api/global/config/RestClientConfig.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/config/RestClientConfig.java index c3b4174..d717ec1 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/config/RestClientConfig.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/config/RestClientConfig.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.config; +package io.wisoft.prepair.prepair_api.common.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/config/S3Config.java b/src/main/java/io/wisoft/prepair/prepair_api/common/config/S3Config.java similarity index 97% rename from src/main/java/io/wisoft/prepair/prepair_api/global/config/S3Config.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/config/S3Config.java index 2164527..fbaa7b5 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/config/S3Config.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/config/S3Config.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.config; +package io.wisoft.prepair.prepair_api.common.config; import java.net.URI; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/BusinessException.java b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/BusinessException.java similarity index 82% rename from src/main/java/io/wisoft/prepair/prepair_api/global/exception/BusinessException.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/exception/BusinessException.java index ecb2cf8..b80bb83 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/BusinessException.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/BusinessException.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.exception; +package io.wisoft.prepair.prepair_api.common.exception; import lombok.Getter; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorCode.java b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorCode.java similarity index 98% rename from src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorCode.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorCode.java index acfc775..45092dc 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorCode.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.exception; +package io.wisoft.prepair.prepair_api.common.exception; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorDetail.java b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorDetail.java similarity index 58% rename from src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorDetail.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorDetail.java index b429c82..b7bca0f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorDetail.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorDetail.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.exception; +package io.wisoft.prepair.prepair_api.common.exception; public record ErrorDetail( String code, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorResponse.java similarity index 84% rename from src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorResponse.java index 00cb36a..c94dec6 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/ErrorResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.exception; +package io.wisoft.prepair.prepair_api.common.exception; public record ErrorResponse( int statusCode, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/GlobalExceptionHandler.java b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/GlobalExceptionHandler.java similarity index 98% rename from src/main/java/io/wisoft/prepair/prepair_api/global/exception/GlobalExceptionHandler.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/exception/GlobalExceptionHandler.java index cc06413..74ea479 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.exception; +package io.wisoft.prepair.prepair_api.common.exception; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/common/ApiResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/common/response/ApiResponse.java similarity index 91% rename from src/main/java/io/wisoft/prepair/prepair_api/global/common/ApiResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/response/ApiResponse.java index 2748bf4..26e366d 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/common/ApiResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/response/ApiResponse.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.common; +package io.wisoft.prepair.prepair_api.common.response; import org.springframework.http.HttpStatus; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/common/BaseTimeEntity.java b/src/main/java/io/wisoft/prepair/prepair_api/common/support/BaseTimeEntity.java similarity index 91% rename from src/main/java/io/wisoft/prepair/prepair_api/global/common/BaseTimeEntity.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/support/BaseTimeEntity.java index 59ac772..3052762 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/common/BaseTimeEntity.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/support/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.common; +package io.wisoft.prepair.prepair_api.common.support; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/filter/CorrelationIdFilter.java b/src/main/java/io/wisoft/prepair/prepair_api/common/support/CorrelationIdFilter.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/global/filter/CorrelationIdFilter.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/support/CorrelationIdFilter.java index 5fedb42..289ffc6 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/filter/CorrelationIdFilter.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/support/CorrelationIdFilter.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.filter; +package io.wisoft.prepair.prepair_api.common.support; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/sse/SseEmitterManager.java b/src/main/java/io/wisoft/prepair/prepair_api/common/support/SseEmitterManager.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/global/sse/SseEmitterManager.java rename to src/main/java/io/wisoft/prepair/prepair_api/common/support/SseEmitterManager.java index 3acec81..0bd2cac 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/sse/SseEmitterManager.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/common/support/SseEmitterManager.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.sse; +package io.wisoft.prepair.prepair_api.common.support; import java.io.IOException; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/controller/InterviewController.java b/src/main/java/io/wisoft/prepair/prepair_api/controller/InterviewController.java deleted file mode 100644 index 3bc5237..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/controller/InterviewController.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.wisoft.prepair.prepair_api.controller; - -import io.wisoft.prepair.prepair_api.dto.request.AnswerRequest; -import io.wisoft.prepair.prepair_api.dto.request.VideoInterviewRequest; -import io.wisoft.prepair.prepair_api.dto.response.FeedbackResponse; -import io.wisoft.prepair.prepair_api.dto.response.QuestionResponse; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; -import io.wisoft.prepair.prepair_api.global.common.ApiResponse; -import io.wisoft.prepair.prepair_api.service.answer.AnswerService; -import io.wisoft.prepair.prepair_api.global.sse.SseEmitterManager; -import io.wisoft.prepair.prepair_api.service.question.QuestionService; -import jakarta.validation.Valid; - -import java.util.List; -import java.util.UUID; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -@Slf4j -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/interviews") -public class InterviewController { - - private final QuestionService questionService; - private final AnswerService answerService; - private final SseEmitterManager sseEmitterManager; - - @GetMapping("/questions") - public ApiResponse> getQuestions( - @RequestHeader("X-User-Id") UUID memberId, - @RequestParam QuestionType type - ) { - List data = questionService.getQuestions(memberId, type) - .stream() - .map(QuestionResponse::from) - .toList(); - - return ApiResponse.ok(data, "질문 목록을 조회했습니다."); - } - - @GetMapping("/questions/{questionId}") - public ApiResponse getQuestion( - @PathVariable UUID questionId, - @RequestHeader("X-User-Id") UUID memberId - ) { - QuestionResponse data = QuestionResponse.from(questionService.getQuestion(questionId, memberId)); - return ApiResponse.ok(data, "특정 질문을 조회했습니다."); - } - - @PostMapping("/questions/video") - @ResponseStatus(HttpStatus.CREATED) - public ApiResponse> generateVideoQuestion( - @RequestHeader("X-User-Id") UUID memberId, - @RequestBody @Valid VideoInterviewRequest request - ) { - List data = questionService.generateVideoQuestions(memberId, request) - .stream() - .map(QuestionResponse::from) - .toList(); - - return ApiResponse.created(data, "화상 면접 질문이 생성되었습니다."); - } - - @PostMapping("/questions/{questionId}/answers") - @ResponseStatus(HttpStatus.CREATED) - public ApiResponse submitAnswer( - @PathVariable UUID questionId, - @RequestHeader("X-User-Id") UUID memberId, - @RequestBody @Valid AnswerRequest request - ) { - FeedbackResponse data = answerService.submitAnswer(questionId, memberId, request.answer()); - return ApiResponse.created(data, "답변이 제출되었습니다."); - } - - @PostMapping(value = "/questions/{questionId}/video-answers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @ResponseStatus(HttpStatus.ACCEPTED) - public ApiResponse submitVideoAnswer( - @PathVariable UUID questionId, - @RequestHeader("X-User-Id") UUID memberId, - @RequestPart("video") MultipartFile video - ) { - answerService.submitVideoAnswer(questionId, memberId, video); - return ApiResponse.accepted(null, "영상 답변이 제출되었습니다."); - } - - @GetMapping(value = "/questions/video-answers/{sessionId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter subscribeSession( - @PathVariable UUID sessionId, - @RequestHeader("X-User-Id") UUID memberId - ) { - questionService.validateSessionOwner(sessionId, memberId); - return sseEmitterManager.create(sessionId); - } -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/controller/JobPostingController.java b/src/main/java/io/wisoft/prepair/prepair_api/controller/JobPostingController.java deleted file mode 100644 index 83a6b55..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/controller/JobPostingController.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.wisoft.prepair.prepair_api.controller; - -import io.wisoft.prepair.prepair_api.dto.request.JobPostingRequest; -import io.wisoft.prepair.prepair_api.dto.response.CompanyQuestionResponse; -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.global.common.ApiResponse; -import io.wisoft.prepair.prepair_api.service.question.QuestionService; -import io.wisoft.prepair.prepair_api.service.company.JobPostingService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.UUID; - -@RestController -@RequestMapping("/api/interviews") -@RequiredArgsConstructor -public class JobPostingController { - - private final JobPostingService jobPostingService; - private final QuestionService interviewService; - - @PostMapping("/questions/company") - public ApiResponse crawl( - @RequestHeader("X-User-Id") final UUID memberId, - @Valid @RequestBody final JobPostingRequest request - ) { - final JobPosting jobPosting = jobPostingService.crawlAndSave(request.url()); - final CompanyQuestionResponse response = CompanyQuestionResponse.of( - jobPosting, - interviewService.generateCompanyQuestions(memberId, jobPosting) - ); - return ApiResponse.ok(response, "기업 맞춤 질문 생성 완료"); - } -} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/CompanyQuestionResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/dto/response/CompanyQuestionResponse.java deleted file mode 100644 index 528b7ba..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/CompanyQuestionResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.wisoft.prepair.prepair_api.dto.response; - -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; - -import java.util.List; - -public record CompanyQuestionResponse( - JobPostingResponse jobPosting, - List questions -) { - - public static CompanyQuestionResponse of(JobPosting jobPosting, List questions) { - return new CompanyQuestionResponse( - JobPostingResponse.from(jobPosting), - questions.stream().map(QuestionResponse::from).toList() - ); - } -} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/AnswerType.java b/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/AnswerType.java deleted file mode 100644 index 87699d4..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/AnswerType.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.wisoft.prepair.prepair_api.entity.enums; - -public enum AnswerType { - TEXT, VIDEO -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionStatus.java b/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionStatus.java deleted file mode 100644 index 39b16f9..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.wisoft.prepair.prepair_api.entity.enums; - -public enum QuestionStatus { - ANSWERED, UNANSWERED -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionType.java b/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionType.java deleted file mode 100644 index 15153f0..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/QuestionType.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.wisoft.prepair.prepair_api.entity.enums; - -public enum QuestionType { - TEXT, VIDEO, COMPANY -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SourceType.java b/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SourceType.java deleted file mode 100644 index 18526e8..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SourceType.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.wisoft.prepair.prepair_api.entity.enums; - -public enum SourceType { - JOBKOREA, SARAMIN, OTHER -} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/MemberServiceClient.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/MemberServiceClient.java similarity index 90% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/member/MemberServiceClient.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/MemberServiceClient.java index c41dcaf..f8d3519 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/MemberServiceClient.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/MemberServiceClient.java @@ -1,10 +1,10 @@ -package io.wisoft.prepair.prepair_api.global.client.member; +package io.wisoft.prepair.prepair_api.external.member; -import io.wisoft.prepair.prepair_api.global.client.member.dto.MemberSchedulerInfo; -import io.wisoft.prepair.prepair_api.global.client.member.dto.MembersData; -import io.wisoft.prepair.prepair_api.global.client.member.dto.MemberServiceResponse; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.external.member.dto.MemberSchedulerInfo; +import io.wisoft.prepair.prepair_api.external.member.dto.MembersData; +import io.wisoft.prepair.prepair_api.external.member.dto.MemberServiceResponse; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.util.Map; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberInfo.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberInfo.java similarity index 60% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberInfo.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberInfo.java index 7ff3b53..9ae6525 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberInfo.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberInfo.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.global.client.member.dto; +package io.wisoft.prepair.prepair_api.external.member.dto; -import io.wisoft.prepair.prepair_api.entity.enums.Frequency; -import io.wisoft.prepair.prepair_api.entity.enums.Notification; +import io.wisoft.prepair.prepair_api.external.member.enums.Frequency; +import io.wisoft.prepair.prepair_api.external.member.enums.Notification; import java.util.List; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberSchedulerInfo.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberSchedulerInfo.java similarity index 61% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberSchedulerInfo.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberSchedulerInfo.java index 20dfd0b..26a0743 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberSchedulerInfo.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberSchedulerInfo.java @@ -1,10 +1,9 @@ -package io.wisoft.prepair.prepair_api.global.client.member.dto; +package io.wisoft.prepair.prepair_api.external.member.dto; -import io.wisoft.prepair.prepair_api.entity.enums.Frequency; -import io.wisoft.prepair.prepair_api.entity.enums.Notification; +import io.wisoft.prepair.prepair_api.external.member.enums.Frequency; +import io.wisoft.prepair.prepair_api.external.member.enums.Notification; import java.util.List; -import java.util.Optional; import java.util.UUID; public record MemberSchedulerInfo( diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberServiceResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberServiceResponse.java similarity index 63% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberServiceResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberServiceResponse.java index f2e6174..160f987 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MemberServiceResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MemberServiceResponse.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.member.dto; +package io.wisoft.prepair.prepair_api.external.member.dto; public record MemberServiceResponse( int statusCode, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MembersData.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MembersData.java similarity index 71% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MembersData.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MembersData.java index c9293b9..d95d94f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/member/dto/MembersData.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/dto/MembersData.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.member.dto; +package io.wisoft.prepair.prepair_api.external.member.dto; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Frequency.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Frequency.java similarity index 71% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Frequency.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Frequency.java index 39a925c..c589bf0 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Frequency.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Frequency.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.entity.enums; +package io.wisoft.prepair.prepair_api.external.member.enums; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Notification.java b/src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Notification.java similarity index 75% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Notification.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Notification.java index 959fe29..a93775d 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/Notification.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/member/enums/Notification.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.entity.enums; +package io.wisoft.prepair.prepair_api.external.member.enums; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailService.java b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java similarity index 87% rename from src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailService.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java index 76246bc..1c078ff 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.notification.email; +package io.wisoft.prepair.prepair_api.external.notification.email; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailTemplateBuilder.java b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailTemplateBuilder.java similarity index 98% rename from src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailTemplateBuilder.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailTemplateBuilder.java index f96d8da..326ad43 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/notification/email/EmailTemplateBuilder.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailTemplateBuilder.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.notification.email; +package io.wisoft.prepair.prepair_api.external.notification.email; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/notification/kakao/KakaoService.java b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java similarity index 95% rename from src/main/java/io/wisoft/prepair/prepair_api/notification/kakao/KakaoService.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java index e0336dd..f882536 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/notification/kakao/KakaoService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.notification.kakao; +package io.wisoft.prepair.prepair_api.external.notification.kakao; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/OpenAiClient.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/OpenAiClient.java similarity index 92% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/OpenAiClient.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/OpenAiClient.java index 906ea6f..6f029ac 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/OpenAiClient.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/OpenAiClient.java @@ -1,14 +1,14 @@ -package io.wisoft.prepair.prepair_api.global.client.openai; +package io.wisoft.prepair.prepair_api.external.openai; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.OpenAiRequest; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.OpenAiResponse; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.QuestionWithTags; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.VideoRequest; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.external.openai.dto.OpenAiRequest; +import io.wisoft.prepair.prepair_api.external.openai.dto.OpenAiResponse; +import io.wisoft.prepair.prepair_api.external.openai.dto.QuestionWithTags; +import io.wisoft.prepair.prepair_api.external.openai.dto.VideoRequest; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/external/openai/WhisperResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/WhisperResponse.java new file mode 100644 index 0000000..f60de51 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/WhisperResponse.java @@ -0,0 +1,4 @@ +package io.wisoft.prepair.prepair_api.external.openai; + +public record WhisperResponse(String text) { +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/Message.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/Message.java similarity index 53% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/Message.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/Message.java index 6a901c2..926e52e 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/Message.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/Message.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; public record Message( String role, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiRequest.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiRequest.java similarity index 82% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiRequest.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiRequest.java index 3243c10..c6dca89 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiRequest.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiRequest.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiResponse.java similarity index 79% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiResponse.java index 3551ab8..652e756 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/OpenAiResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/OpenAiResponse.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/QuestionWithTags.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/QuestionWithTags.java similarity index 74% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/QuestionWithTags.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/QuestionWithTags.java index 73bcc99..16608f3 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/QuestionWithTags.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/QuestionWithTags.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoMessage.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoMessage.java similarity index 91% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoMessage.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoMessage.java index 20fea7c..e32a4ee 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoMessage.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoMessage.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoRequest.java b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoRequest.java similarity index 92% rename from src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoRequest.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoRequest.java index 4d45dc4..ae84100 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/dto/VideoRequest.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/openai/dto/VideoRequest.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.global.client.openai.dto; +package io.wisoft.prepair.prepair_api.external.openai.dto; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/storage/FileUploader.java b/src/main/java/io/wisoft/prepair/prepair_api/external/storage/FileUploader.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/storage/FileUploader.java rename to src/main/java/io/wisoft/prepair/prepair_api/external/storage/FileUploader.java index a03a75f..f6a504f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/storage/FileUploader.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/storage/FileUploader.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.storage; +package io.wisoft.prepair.prepair_api.external.storage; import java.nio.file.Path; import java.time.LocalDate; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/WhisperResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/WhisperResponse.java deleted file mode 100644 index e85404b..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/global/client/openai/WhisperResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.wisoft.prepair.prepair_api.global.client.openai; - -public record WhisperResponse(String text) { -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/controller/AnswerController.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/controller/AnswerController.java new file mode 100644 index 0000000..c984f10 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/controller/AnswerController.java @@ -0,0 +1,56 @@ +package io.wisoft.prepair.prepair_api.interview.answer.controller; + +import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerRequest; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResponse; +import io.wisoft.prepair.prepair_api.common.response.ApiResponse; +import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerService; +import io.wisoft.prepair.prepair_api.interview.answer.service.VideoAnswerSseService; +import jakarta.validation.Valid; + +import java.util.UUID; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/interviews/questions") +public class AnswerController { + + private final AnswerService answerService; + private final VideoAnswerSseService videoAnswerStreamService; + + @PostMapping("/{questionId}/answers") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse submitAnswer( + @PathVariable UUID questionId, + @RequestHeader("X-User-Id") UUID memberId, + @RequestBody @Valid AnswerRequest request + ) { + FeedbackResponse data = answerService.submitAnswer(questionId, memberId, request.answer()); + return ApiResponse.created(data, "답변이 제출되었습니다."); + } + + @PostMapping(value = "/{questionId}/video-answers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.ACCEPTED) + public ApiResponse submitVideoAnswer( + @PathVariable UUID questionId, + @RequestHeader("X-User-Id") UUID memberId, + @RequestPart("video") MultipartFile video + ) { + answerService.submitVideoAnswer(questionId, memberId, video); + return ApiResponse.accepted(null, "영상 답변이 제출되었습니다."); + } + + @GetMapping(value = "/video-answers/{sessionId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribeSession( + @PathVariable UUID sessionId, + @RequestHeader("X-User-Id") UUID memberId + ) { + return videoAnswerStreamService.subscribe(sessionId, memberId); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/AnswerRequest.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerRequest.java similarity index 66% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/request/AnswerRequest.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerRequest.java index 2a2162d..80d46d2 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/AnswerRequest.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerRequest.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto.request; +package io.wisoft.prepair.prepair_api.interview.answer.dto; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerSubmitResult.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerSubmitResult.java new file mode 100644 index 0000000..fddaad3 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/AnswerSubmitResult.java @@ -0,0 +1,9 @@ +package io.wisoft.prepair.prepair_api.interview.answer.dto; + +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; + +public record AnswerSubmitResult( + InterviewFeedback feedback, + boolean firstAnswer +) { +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/CombinedFeedbackResult.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/CombinedFeedbackResult.java similarity index 61% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/CombinedFeedbackResult.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/CombinedFeedbackResult.java index 5610d6a..38e2c08 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/CombinedFeedbackResult.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/CombinedFeedbackResult.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto; +package io.wisoft.prepair.prepair_api.interview.answer.dto; public record CombinedFeedbackResult( String combineFeedback, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackDetail.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackDetail.java similarity index 65% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackDetail.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackDetail.java index 92fcf4a..44290c4 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackDetail.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackDetail.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto.response; +package io.wisoft.prepair.prepair_api.interview.answer.dto; public record FeedbackDetail( String good, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResponse.java similarity index 73% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResponse.java index 72ab429..90a5cce 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FeedbackResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResponse.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.dto.response; +package io.wisoft.prepair.prepair_api.interview.answer.dto; -import io.wisoft.prepair.prepair_api.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; import java.time.LocalDateTime; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/FeedbackResult.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResult.java similarity index 69% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/FeedbackResult.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResult.java index c2fdc3e..6b4fa18 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/FeedbackResult.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FeedbackResult.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto; +package io.wisoft.prepair.prepair_api.interview.answer.dto; public record FeedbackResult( String good, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FinalFeedbackResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResponse.java similarity index 88% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/response/FinalFeedbackResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResponse.java index 9578402..958062b 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/FinalFeedbackResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResponse.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto.response; +package io.wisoft.prepair.prepair_api.interview.answer.dto; import java.util.List; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/FinalFeedbackResult.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResult.java similarity index 53% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/FinalFeedbackResult.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResult.java index ba261f4..6d175f5 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/FinalFeedbackResult.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/dto/FinalFeedbackResult.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto; +package io.wisoft.prepair.prepair_api.interview.answer.dto; public record FinalFeedbackResult( String finalFeedback diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/AnswerType.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/AnswerType.java new file mode 100644 index 0000000..91df2f8 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/AnswerType.java @@ -0,0 +1,5 @@ +package io.wisoft.prepair.prepair_api.interview.answer.entity; + +public enum AnswerType { + TEXT, VIDEO +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/FeedbackType.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/FeedbackType.java similarity index 51% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/enums/FeedbackType.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/FeedbackType.java index efafbaf..53cae48 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/FeedbackType.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/FeedbackType.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.entity.enums; +package io.wisoft.prepair.prepair_api.interview.answer.entity; public enum FeedbackType { TEXT, STT, VIDEO, COMBINED, FINAL diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewAnswer.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewAnswer.java similarity index 85% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewAnswer.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewAnswer.java index 25f51d5..3569c1f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewAnswer.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewAnswer.java @@ -1,7 +1,8 @@ -package io.wisoft.prepair.prepair_api.entity; +package io.wisoft.prepair.prepair_api.interview.answer.entity; -import io.wisoft.prepair.prepair_api.entity.enums.AnswerType; -import io.wisoft.prepair.prepair_api.global.common.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.answer.entity.AnswerType; +import io.wisoft.prepair.prepair_api.common.support.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewFeedback.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewFeedback.java similarity index 84% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewFeedback.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewFeedback.java index 1f56201..9d97f05 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewFeedback.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/entity/InterviewFeedback.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.entity; +package io.wisoft.prepair.prepair_api.interview.answer.entity; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; -import io.wisoft.prepair.prepair_api.global.common.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; +import io.wisoft.prepair.prepair_api.common.support.BaseTimeEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedEvent.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedEvent.java similarity index 70% rename from src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedEvent.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedEvent.java index a6c7822..3a88810 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedEvent.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedEvent.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.service.answer.event; +package io.wisoft.prepair.prepair_api.interview.answer.event; import java.nio.file.Path; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedHandler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java similarity index 83% rename from src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedHandler.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java index 4959685..d193a45 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AllAnalysisCompletedHandler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AllAnalysisCompletedHandler.java @@ -1,23 +1,23 @@ -package io.wisoft.prepair.prepair_api.service.answer.event; +package io.wisoft.prepair.prepair_api.interview.answer.event; -import io.wisoft.prepair.prepair_api.dto.CombinedFeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import io.wisoft.prepair.prepair_api.dto.FinalFeedbackResult; -import io.wisoft.prepair.prepair_api.dto.response.FinalFeedbackResponse; -import io.wisoft.prepair.prepair_api.entity.InterviewAnswer; -import io.wisoft.prepair.prepair_api.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.InterviewSession; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; -import io.wisoft.prepair.prepair_api.repository.AnswerRepository; -import io.wisoft.prepair.prepair_api.repository.FeedbackRepository; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; -import io.wisoft.prepair.prepair_api.repository.SessionRepository; -import io.wisoft.prepair.prepair_api.service.answer.AnswerPersistService; -import io.wisoft.prepair.prepair_api.service.answer.FeedbackGenerator; -import io.wisoft.prepair.prepair_api.global.sse.SseEmitterManager; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResponse; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; +import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository; +import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; +import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; +import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerPersistenceService; +import io.wisoft.prepair.prepair_api.interview.answer.service.FeedbackGenerator; +import io.wisoft.prepair.prepair_api.common.support.SseEmitterManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -37,7 +37,7 @@ public class AllAnalysisCompletedHandler { private final FeedbackRepository feedbackRepository; private final FeedbackGenerator feedbackGenerator; - private final AnswerPersistService answerPersistService; + private final AnswerPersistenceService answerPersistenceService; private final AnswerRepository answerRepository; private final QuestionRepository questionRepository; private final SessionRepository sessionRepository; @@ -86,7 +86,7 @@ public void handle(AllAnalysisCompletedEvent event) { videoFeedback.getFeedback() ); - answerPersistService.saveCombinedFeedback(answerId, result); + answerPersistenceService.saveCombinedFeedback(answerId, result); log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score()); checkAndGenerateFinal(answer); diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AnalysisCompletionTracker.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletionTracker.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AnalysisCompletionTracker.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletionTracker.java index b6cadc9..3363870 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/event/AnalysisCompletionTracker.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/event/AnalysisCompletionTracker.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.service.answer.event; +package io.wisoft.prepair.prepair_api.interview.answer.event; import java.nio.file.Path; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/repository/AnswerRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java similarity index 80% rename from src/main/java/io/wisoft/prepair/prepair_api/repository/AnswerRepository.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java index 33cc258..1c99c6c 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/repository/AnswerRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java @@ -1,6 +1,6 @@ -package io.wisoft.prepair.prepair_api.repository; +package io.wisoft.prepair.prepair_api.interview.answer.repository; -import io.wisoft.prepair.prepair_api.entity.InterviewAnswer; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/repository/FeedbackRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java similarity index 77% rename from src/main/java/io/wisoft/prepair/prepair_api/repository/FeedbackRepository.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java index fd4b4c5..b760640 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/repository/FeedbackRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.repository; +package io.wisoft.prepair.prepair_api.interview.answer.repository; -import io.wisoft.prepair.prepair_api.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; import java.util.List; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerPersistenceService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerPersistenceService.java new file mode 100644 index 0000000..9f0f622 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerPersistenceService.java @@ -0,0 +1,119 @@ +package io.wisoft.prepair.prepair_api.interview.answer.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerSubmitResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.answer.entity.AnswerType; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionStatus; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository; +import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class AnswerPersistenceService { + + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + private final FeedbackRepository feedbackRepository; + private final ObjectMapper objectMapper; + + public AnswerSubmitResult saveAnswerAndFeedback(final UUID questionId, final UUID memberId, final String answerText, final FeedbackResult result, final FeedbackDetail detail) { + InterviewQuestion question = getQuestion(questionId, memberId); + question.updateStatus(QuestionStatus.ANSWERED); + + // 답변 + 피드백 저장 + InterviewAnswer answer = saveAnswer(answerText, question); + InterviewFeedback feedback = saveTextFeedback(result, detail, answer); + + // 조건부 UPDATE로 최초 답변 여부 원자적 판단 (race condition 방지) + boolean firstAnswer = isFirstTodayAnswer(questionId, feedback.getScore()); + + // sendScore는 트랜잭션 외부에서 호출하므로 firstAnswer를 함께 반환 + return new AnswerSubmitResult(feedback, firstAnswer); + } + + public InterviewAnswer saveVideoAnswer(final UUID questionId, final UUID memberId) { + InterviewQuestion question = getQuestion(questionId, memberId); + question.updateStatus(QuestionStatus.ANSWERED); + return answerRepository.save(new InterviewAnswer(question, "", AnswerType.VIDEO, null)); + } + + public void updateMediaUrl(final UUID answerId, final String mediaUrl) { + InterviewAnswer interviewAnswer = getAnswer(answerId); + interviewAnswer.updateMediaUrl(mediaUrl); + } + + public void updateAnswer(final UUID answerId, final String answer) { + InterviewAnswer interviewAnswer = getAnswer(answerId); + interviewAnswer.updateAnswer(answer); + } + + public void saveVideoFeedback(final UUID answerId, final FeedbackResult result, final FeedbackDetail detail, final FeedbackType feedbackType) { + InterviewAnswer interviewAnswer = getAnswer(answerId); + feedbackRepository.save(new InterviewFeedback(interviewAnswer, serializeFeedback(detail), feedbackType, result.score())); + } + + public void saveCombinedFeedback(final UUID answerId, final CombinedFeedbackResult result) { + InterviewAnswer interviewAnswer = getAnswer(answerId); + feedbackRepository.save(new InterviewFeedback(interviewAnswer, result.combineFeedback(), FeedbackType.COMBINED, result.score())); + } + + private InterviewQuestion getQuestion(UUID questionId, UUID memberId) { + return questionRepository.findByIdAndMemberId(questionId, memberId) + .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); + } + + private InterviewAnswer saveAnswer(String answer, InterviewQuestion question) { + return answerRepository.save( + new InterviewAnswer(question, answer, AnswerType.TEXT, null) + ); + } + + private InterviewFeedback saveTextFeedback(FeedbackResult result, FeedbackDetail detail, InterviewAnswer interviewAnswer) { + return feedbackRepository.save( + new InterviewFeedback(interviewAnswer, serializeFeedback(detail), FeedbackType.TEXT, result.score()) + ); + } + + private boolean isFirstTodayAnswer(final UUID questionId, final Integer score) { + LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); + LocalDateTime endOfDay = startOfDay.plusDays(1); + return questionRepository.updateLatestScoreIfFirstTime(questionId, score, startOfDay, endOfDay) > 0; + } + + private InterviewAnswer getAnswer(UUID answerId) { + return answerRepository.findById(answerId) + .orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND)); + } + + private String serializeFeedback(final FeedbackDetail detail) { + try { + return objectMapper.writeValueAsString(detail); + } catch (JsonProcessingException e) { + log.error("피드백 직렬화 실패", e); + throw new BusinessException(ErrorCode.INTERNAL_ERROR); + } + } +} + diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerService.java new file mode 100644 index 0000000..d1f78d7 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/AnswerService.java @@ -0,0 +1,100 @@ +package io.wisoft.prepair.prepair_api.interview.answer.service; + +import io.wisoft.prepair.prepair_api.interview.answer.dto.AnswerSubmitResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResponse; +import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.external.member.MemberServiceClient; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; +import io.wisoft.prepair.prepair_api.interview.answer.event.AnalysisCompletionTracker; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AnswerService { + + private final AnswerPersistenceService answerPersistenceService; + private final VideoAnswerProcessor videoAnswerAnalyzer; + private final FeedbackGenerator feedbackGenerator; + private final QuestionRepository questionRepository; + private final MemberServiceClient memberServiceClient; + private final AnalysisCompletionTracker completionTracker; + + public FeedbackResponse submitAnswer(final UUID questionId, final UUID memberId, final String answer) { + // AI 피드백 생성 + InterviewQuestion question = getQuestion(questionId, memberId); + FeedbackResult result = feedbackGenerator.generate(question, answer); + FeedbackDetail detail = new FeedbackDetail(result.good(), result.improvement(), result.recommendation()); + + // 답변 + 피드백 저장 (최초 답변 여부 원자적 판단 포함) + AnswerSubmitResult answerSubmitResult = answerPersistenceService.saveAnswerAndFeedback( + questionId, memberId, answer, result, detail + ); + + log.info("답변 제출 및 피드백 생성 완료 - questionId: {}, score: {}", questionId, result.score()); + + // 오늘의 질문 최초 답변 시 점수 전송 (트랜잭션 외부) + if (answerSubmitResult.firstAnswer()) { + memberServiceClient.sendScore(memberId, answerSubmitResult.feedback().getScore()); + } + + return FeedbackResponse.from(answerSubmitResult.feedback(), detail); + } + + public void submitVideoAnswer(final UUID questionId, final UUID memberId, final MultipartFile video) { + // S3 업로드에 필요한 이메일 조회 + String email = memberServiceClient.getMember(memberId).email(); + + // 임시 파일 먼저 생성 후 DB 저장 (실패 시 고아 레코드 방지) + Path videoPath = createTempFile(video); + InterviewAnswer answer = answerPersistenceService.saveVideoAnswer(questionId, memberId); + + // 3개 비동기 작업 모두 완료 시 이벤트 발행을 위한 트래커 초기화 + completionTracker.init(answer.getId(), videoPath); + log.info("영상 답변 분석 시작 - questionId: {}, answerId: {}", questionId, answer.getId()); + + videoAnswerAnalyzer.uploadToS3(answer.getId(), videoPath, video.getContentType(), email); + videoAnswerAnalyzer.analyzeSTT(answer.getId(), questionId, memberId, videoPath); + videoAnswerAnalyzer.analyzeVideo(answer.getId(), videoPath); + } + + private InterviewQuestion getQuestion(UUID questionId, UUID memberId) { + return questionRepository.findByIdAndMemberId(questionId, memberId) + .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); + } + + private Path createTempFile(final MultipartFile video) { + try { + Path videoPath = Files.createTempFile("video-", getExtension(video.getOriginalFilename())); + video.transferTo(videoPath); + return videoPath; + } catch (Exception e) { + log.error("영상 임시 파일 생성 실패 - filename: {}", video.getOriginalFilename(), e); + throw new BusinessException(ErrorCode.INTERNAL_ERROR); + } + } + + private String getExtension(final String filename) { + if (filename == null || filename.isBlank()) { + return ".tmp"; + } + + int extensionIndex = filename.lastIndexOf('.'); + if (extensionIndex < 0 || extensionIndex == filename.length() - 1) { + return ".tmp"; + } + + return filename.substring(extensionIndex); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/FeedbackGenerator.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FeedbackGenerator.java similarity index 78% rename from src/main/java/io/wisoft/prepair/prepair_api/service/answer/FeedbackGenerator.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FeedbackGenerator.java index 265ba39..0a588dc 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/FeedbackGenerator.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FeedbackGenerator.java @@ -1,15 +1,15 @@ -package io.wisoft.prepair.prepair_api.service.answer; +package io.wisoft.prepair.prepair_api.interview.answer.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.wisoft.prepair.prepair_api.dto.CombinedFeedbackResult; -import io.wisoft.prepair.prepair_api.dto.FeedbackResult; -import io.wisoft.prepair.prepair_api.dto.FinalFeedbackResult; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.prompt.PromptBuilder; +import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.prompt.PromptBuilder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/video/FrameExtractor.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FrameExtractor.java similarity index 95% rename from src/main/java/io/wisoft/prepair/prepair_api/video/FrameExtractor.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FrameExtractor.java index ccd4c68..ba59812 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/video/FrameExtractor.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/FrameExtractor.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.video; +package io.wisoft.prepair.prepair_api.interview.answer.service; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.io.File; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/stt/SpeechToTextService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/SpeechToTextService.java similarity index 86% rename from src/main/java/io/wisoft/prepair/prepair_api/service/stt/SpeechToTextService.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/SpeechToTextService.java index 6e5ea6b..c00a9ae 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/stt/SpeechToTextService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/SpeechToTextService.java @@ -1,8 +1,8 @@ -package io.wisoft.prepair.prepair_api.service.stt; +package io.wisoft.prepair.prepair_api.interview.answer.service; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/VideoAnswerAnalyzer.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerProcessor.java similarity index 65% rename from src/main/java/io/wisoft/prepair/prepair_api/service/answer/VideoAnswerAnalyzer.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerProcessor.java index ed6290b..950f008 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/VideoAnswerAnalyzer.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerProcessor.java @@ -1,16 +1,14 @@ -package io.wisoft.prepair.prepair_api.service.answer; +package io.wisoft.prepair.prepair_api.interview.answer.service; -import io.wisoft.prepair.prepair_api.dto.FeedbackResult; -import io.wisoft.prepair.prepair_api.dto.response.FeedbackDetail; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; -import io.wisoft.prepair.prepair_api.service.answer.event.AnalysisCompletionTracker; -import io.wisoft.prepair.prepair_api.service.stt.SpeechToTextService; -import io.wisoft.prepair.prepair_api.service.vidoe.VideoAnalysisService; -import io.wisoft.prepair.prepair_api.storage.FileUploader; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackDetail; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; +import io.wisoft.prepair.prepair_api.interview.answer.event.AnalysisCompletionTracker; +import io.wisoft.prepair.prepair_api.external.storage.FileUploader; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -22,11 +20,11 @@ @Slf4j @Service @RequiredArgsConstructor -public class VideoAnswerAnalyzer { +public class VideoAnswerProcessor { - private final AnswerPersistService answerPersistService; + private final AnswerPersistenceService answerPersistenceService; private final SpeechToTextService speechToTextService; - private final VideoAnalysisService videoAnalysisService; + private final VideoFrameAnalysisService videoAnalysisService; private final QuestionRepository questionRepository; private final FileUploader fileUploader; private final FeedbackGenerator feedbackGenerator; @@ -36,7 +34,7 @@ public class VideoAnswerAnalyzer { public void uploadToS3(final UUID answerId, final Path videoPath, final String contentType, final String email) { try { String mediaUrl = fileUploader.upload(videoPath, contentType, email); - answerPersistService.updateMediaUrl(answerId, mediaUrl); + answerPersistenceService.updateMediaUrl(answerId, mediaUrl); log.info("[VIDEO-S3] 업로드 완료 - answerId: {}", answerId); completionTracker.complete(answerId); } catch (Exception e) { @@ -46,19 +44,19 @@ public void uploadToS3(final UUID answerId, final Path videoPath, final String c } @Async("videoTaskExecutor") - public void analyzeSTT(final UUID answerId, final UUID questionId, final UUID memberId, - final Path videoPath, final String questionTags) { + public void analyzeSTT(final UUID answerId, final UUID questionId, final UUID memberId, final Path videoPath) { try { - String answer = speechToTextService.convertToTextFromPath(videoPath, questionTags); - answerPersistService.updateAnswer(answerId, answer); - InterviewQuestion question = questionRepository.findByIdAndMemberId(questionId, memberId) .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); + String answer = speechToTextService.convertToTextFromPath(videoPath, question.getQuestionTag()); + answerPersistenceService.updateAnswer(answerId, answer); + FeedbackResult result = feedbackGenerator.generate(question, answer); FeedbackDetail detail = new FeedbackDetail(result.good(), result.improvement(), result.recommendation()); - answerPersistService.saveFeedback(answerId, result, detail, FeedbackType.STT); + answerPersistenceService.saveVideoFeedback(answerId, result, detail, FeedbackType.STT); + log.info("[VIDEO-STT] 분석 완료 - answerId: {}", answerId); completionTracker.complete(answerId); } catch (Exception e) { @@ -73,7 +71,8 @@ public void analyzeVideo(final UUID answerId, final Path videoPath) { FeedbackResult result = videoAnalysisService.analyze(videoPath); FeedbackDetail detail = new FeedbackDetail(result.good(), result.improvement(), result.recommendation()); - answerPersistService.saveFeedback(answerId, result, detail, FeedbackType.VIDEO); + answerPersistenceService.saveVideoFeedback(answerId, result, detail, FeedbackType.VIDEO); + log.info("[VIDEO-ANALYSIS] 분석 완료 - answerId: {}", answerId); completionTracker.complete(answerId); } catch (Exception e) { diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerSseService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerSseService.java new file mode 100644 index 0000000..f444cc4 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoAnswerSseService.java @@ -0,0 +1,33 @@ +package io.wisoft.prepair.prepair_api.interview.answer.service; + +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.common.support.SseEmitterManager; +import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Service +@RequiredArgsConstructor +public class VideoAnswerSseService { + + private final SessionRepository sessionRepository; + private final SseEmitterManager sseEmitterManager; + + public SseEmitter subscribe(final UUID sessionId, final UUID memberId) { + validateSessionOwner(sessionId, memberId); + return sseEmitterManager.create(sessionId); + } + + private void validateSessionOwner(final UUID sessionId, final UUID memberId) { + InterviewSession session = sessionRepository.findById(sessionId) + .orElseThrow(() -> new BusinessException(ErrorCode.SESSION_NOT_FOUND)); + + if (!session.getMemberId().equals(memberId)) { + throw new BusinessException(ErrorCode.FORBIDDEN); + } + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/vidoe/VideoAnalysisService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoFrameAnalysisService.java similarity index 86% rename from src/main/java/io/wisoft/prepair/prepair_api/service/vidoe/VideoAnalysisService.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoFrameAnalysisService.java index 4d8be8a..a3d56bc 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/vidoe/VideoAnalysisService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/answer/service/VideoFrameAnalysisService.java @@ -1,13 +1,13 @@ -package io.wisoft.prepair.prepair_api.service.vidoe; +package io.wisoft.prepair.prepair_api.interview.answer.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import io.wisoft.prepair.prepair_api.dto.FeedbackResult; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.prompt.VideoAnalysisPromptBuilder; -import io.wisoft.prepair.prepair_api.video.FrameExtractor; +import io.wisoft.prepair.prepair_api.interview.answer.dto.FeedbackResult; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.prompt.VideoAnalysisPromptBuilder; + import java.nio.file.Path; import java.util.List; import lombok.RequiredArgsConstructor; @@ -17,7 +17,7 @@ @Slf4j @Service @RequiredArgsConstructor -public class VideoAnalysisService { +public class VideoFrameAnalysisService { private final FrameExtractor frameExtractor; private final OpenAiClient openAiClient; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/crawler/CrawlerFactory.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/CrawlerFactory.java similarity index 91% rename from src/main/java/io/wisoft/prepair/prepair_api/crawler/CrawlerFactory.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/CrawlerFactory.java index c7279fb..47be923 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/crawler/CrawlerFactory.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/CrawlerFactory.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.crawler; +package io.wisoft.prepair.prepair_api.interview.jobposting.crawler; import java.util.List; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/crawler/JobKoreaCrawler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobKoreaCrawler.java similarity index 87% rename from src/main/java/io/wisoft/prepair/prepair_api/crawler/JobKoreaCrawler.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobKoreaCrawler.java index 95da741..18bb800 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/crawler/JobKoreaCrawler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobKoreaCrawler.java @@ -1,8 +1,8 @@ -package io.wisoft.prepair.prepair_api.crawler; +package io.wisoft.prepair.prepair_api.interview.jobposting.crawler; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.io.IOException; import java.net.URI; import java.util.regex.Matcher; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/crawler/JobPostingCrawler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobPostingCrawler.java similarity index 60% rename from src/main/java/io/wisoft/prepair/prepair_api/crawler/JobPostingCrawler.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobPostingCrawler.java index 18c3f63..09d15a4 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/crawler/JobPostingCrawler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/JobPostingCrawler.java @@ -1,6 +1,6 @@ -package io.wisoft.prepair.prepair_api.crawler; +package io.wisoft.prepair.prepair_api.interview.jobposting.crawler; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; public interface JobPostingCrawler { diff --git a/src/main/java/io/wisoft/prepair/prepair_api/crawler/SaraminCrawler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/SaraminCrawler.java similarity index 88% rename from src/main/java/io/wisoft/prepair/prepair_api/crawler/SaraminCrawler.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/SaraminCrawler.java index 2141823..c364288 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/crawler/SaraminCrawler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/SaraminCrawler.java @@ -1,8 +1,8 @@ -package io.wisoft.prepair.prepair_api.crawler; +package io.wisoft.prepair.prepair_api.interview.jobposting.crawler; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.io.IOException; import java.net.URI; import java.net.URLDecoder; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/crawler/ScreenshotCrawler.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/ScreenshotCrawler.java similarity index 85% rename from src/main/java/io/wisoft/prepair/prepair_api/crawler/ScreenshotCrawler.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/ScreenshotCrawler.java index dd4af7b..fdda00f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/crawler/ScreenshotCrawler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/crawler/ScreenshotCrawler.java @@ -1,13 +1,13 @@ -package io.wisoft.prepair.prepair_api.crawler; +package io.wisoft.prepair.prepair_api.interview.jobposting.crawler; import com.microsoft.playwright.Browser; import com.microsoft.playwright.Browser.NewContextOptions; import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.Page; import com.microsoft.playwright.Playwright; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/JobPostingRequest.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingRequest.java similarity index 81% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/request/JobPostingRequest.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingRequest.java index 878999e..cd4ab66 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/JobPostingRequest.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingRequest.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto.request; +package io.wisoft.prepair.prepair_api.interview.jobposting.dto; import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.URL; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/JobPostingResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingResponse.java similarity index 70% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/response/JobPostingResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingResponse.java index 7afdb5c..4d8aee3 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/JobPostingResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/dto/JobPostingResponse.java @@ -1,8 +1,8 @@ -package io.wisoft.prepair.prepair_api.dto.response; +package io.wisoft.prepair.prepair_api.interview.jobposting.dto; import com.fasterxml.jackson.annotation.JsonRawValue; -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/JobPosting.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/JobPosting.java similarity index 86% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/JobPosting.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/JobPosting.java index 12b4e4f..37e4c0b 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/JobPosting.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/JobPosting.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.entity; +package io.wisoft.prepair.prepair_api.interview.jobposting.entity; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; -import io.wisoft.prepair.prepair_api.global.common.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; +import io.wisoft.prepair.prepair_api.common.support.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/SourceType.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/SourceType.java new file mode 100644 index 0000000..74c19f2 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/entity/SourceType.java @@ -0,0 +1,5 @@ +package io.wisoft.prepair.prepair_api.interview.jobposting.entity; + +public enum SourceType { + JOBKOREA, SARAMIN, OTHER +} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/repository/JobPostingRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/repository/JobPostingRepository.java similarity index 63% rename from src/main/java/io/wisoft/prepair/prepair_api/repository/JobPostingRepository.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/repository/JobPostingRepository.java index 02effc5..bb0d796 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/repository/JobPostingRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/repository/JobPostingRepository.java @@ -1,6 +1,6 @@ -package io.wisoft.prepair.prepair_api.repository; +package io.wisoft.prepair.prepair_api.interview.jobposting.repository; -import io.wisoft.prepair.prepair_api.entity.JobPosting; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/company/JobPostingService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/service/JobPostingService.java similarity index 63% rename from src/main/java/io/wisoft/prepair/prepair_api/service/company/JobPostingService.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/service/JobPostingService.java index 94df25d..fee92c1 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/company/JobPostingService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/jobposting/service/JobPostingService.java @@ -1,12 +1,12 @@ -package io.wisoft.prepair.prepair_api.service.company; +package io.wisoft.prepair.prepair_api.interview.jobposting.service; -import io.wisoft.prepair.prepair_api.crawler.CrawlerFactory; -import io.wisoft.prepair.prepair_api.crawler.JobPostingCrawler; -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.entity.enums.SourceType; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.prompt.JobPostingPromptBuilder; -import io.wisoft.prepair.prepair_api.repository.JobPostingRepository; +import io.wisoft.prepair.prepair_api.interview.jobposting.crawler.CrawlerFactory; +import io.wisoft.prepair.prepair_api.interview.jobposting.crawler.JobPostingCrawler; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.SourceType; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.interview.prompt.JobPostingPromptBuilder; +import io.wisoft.prepair.prepair_api.interview.jobposting.repository.JobPostingRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/prompt/JobPostingPromptBuilder.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/JobPostingPromptBuilder.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/prompt/JobPostingPromptBuilder.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/JobPostingPromptBuilder.java index bb48595..801f760 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/prompt/JobPostingPromptBuilder.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/JobPostingPromptBuilder.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.prompt; +package io.wisoft.prepair.prepair_api.interview.prompt; import org.springframework.stereotype.Component; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/prompt/PromptBuilder.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/PromptBuilder.java similarity index 98% rename from src/main/java/io/wisoft/prepair/prepair_api/prompt/PromptBuilder.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/PromptBuilder.java index 9c83005..7d5d048 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/prompt/PromptBuilder.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/PromptBuilder.java @@ -1,6 +1,6 @@ -package io.wisoft.prepair.prepair_api.prompt; +package io.wisoft.prepair.prepair_api.interview.prompt; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; import org.springframework.stereotype.Component; import java.util.List; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/prompt/VideoAnalysisPromptBuilder.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/VideoAnalysisPromptBuilder.java similarity index 96% rename from src/main/java/io/wisoft/prepair/prepair_api/prompt/VideoAnalysisPromptBuilder.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/VideoAnalysisPromptBuilder.java index bbbd709..aea3fed 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/prompt/VideoAnalysisPromptBuilder.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/prompt/VideoAnalysisPromptBuilder.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.prompt; +package io.wisoft.prepair.prepair_api.interview.prompt; import org.springframework.stereotype.Component; @@ -32,4 +32,4 @@ public String buildVisionPrompt() { } """; } -} \ No newline at end of file +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/controller/QuestionController.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/controller/QuestionController.java new file mode 100644 index 0000000..e92cd48 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/controller/QuestionController.java @@ -0,0 +1,63 @@ +package io.wisoft.prepair.prepair_api.interview.question.controller; + + +import io.wisoft.prepair.prepair_api.interview.jobposting.dto.JobPostingRequest; +import io.wisoft.prepair.prepair_api.interview.question.dto.VideoInterviewRequest; +import io.wisoft.prepair.prepair_api.interview.question.dto.CompanyQuestionResponse; +import io.wisoft.prepair.prepair_api.interview.question.dto.QuestionResponse; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; +import io.wisoft.prepair.prepair_api.common.response.ApiResponse; +import io.wisoft.prepair.prepair_api.interview.question.service.QuestionService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/interviews/questions") +public class QuestionController { + + private final QuestionService questionService; + + @GetMapping + public ApiResponse> getQuestions( + @RequestHeader("X-User-Id") UUID memberId, + @RequestParam QuestionType type + ) { + List data = questionService.getQuestions(memberId, type); + return ApiResponse.ok(data, "질문 목록을 조회했습니다."); + } + + @GetMapping("/{questionId}") + public ApiResponse getQuestion( + @PathVariable UUID questionId, + @RequestHeader("X-User-Id") UUID memberId + ) { + QuestionResponse data = questionService.getQuestion(questionId, memberId); + return ApiResponse.ok(data, "질문 단건을 조회했습니다."); + } + + @PostMapping("/company") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse generateCompanyQuestions( + @RequestHeader("X-User-Id") UUID memberId, + @RequestBody @Valid JobPostingRequest request + ) { + CompanyQuestionResponse data = questionService.generateCompanyQuestions(memberId, request); + return ApiResponse.created(data, "기업 맞춤 질문 생성되었습니다."); + } + + @PostMapping("/video") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse> generateVideoQuestion( + @RequestHeader("X-User-Id") UUID memberId, + @RequestBody @Valid VideoInterviewRequest request + ) { + List data = questionService.generateVideoQuestions(memberId, request); + return ApiResponse.created(data, "화상 면접 질문이 생성되었습니다."); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/CompanyQuestionResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/CompanyQuestionResponse.java new file mode 100644 index 0000000..f2566c3 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/CompanyQuestionResponse.java @@ -0,0 +1,17 @@ +package io.wisoft.prepair.prepair_api.interview.question.dto; + +import io.wisoft.prepair.prepair_api.interview.jobposting.dto.JobPostingResponse; + +import java.util.List; + +public record CompanyQuestionResponse( + JobPostingResponse jobPosting, + List questions +) { + public static CompanyQuestionResponse of(JobPostingResponse jobPosting, List questions) { + return new CompanyQuestionResponse( + jobPosting, + questions + ); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/QuestionResponse.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/QuestionResponse.java similarity index 75% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/response/QuestionResponse.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/QuestionResponse.java index f6cd1e5..9a6d1bd 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/response/QuestionResponse.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/QuestionResponse.java @@ -1,8 +1,8 @@ -package io.wisoft.prepair.prepair_api.dto.response; +package io.wisoft.prepair.prepair_api.interview.question.dto; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionStatus; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionStatus; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; import java.time.LocalDateTime; import java.util.UUID; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/VideoInterviewRequest.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/VideoInterviewRequest.java similarity index 80% rename from src/main/java/io/wisoft/prepair/prepair_api/dto/request/VideoInterviewRequest.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/VideoInterviewRequest.java index 6bbf755..d5b0778 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/dto/request/VideoInterviewRequest.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/dto/VideoInterviewRequest.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.dto.request; +package io.wisoft.prepair.prepair_api.interview.question.dto; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; @@ -8,5 +8,4 @@ public record VideoInterviewRequest( @Min(value = 1, message = "면접 질문을 1개 이상부터 가능합니다.") Integer count ) { - } diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewQuestion.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/InterviewQuestion.java similarity index 77% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewQuestion.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/InterviewQuestion.java index df44af2..2d80045 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewQuestion.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/InterviewQuestion.java @@ -1,8 +1,10 @@ -package io.wisoft.prepair.prepair_api.entity; +package io.wisoft.prepair.prepair_api.interview.question.entity; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionStatus; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; -import io.wisoft.prepair.prepair_api.global.common.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionStatus; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; +import io.wisoft.prepair.prepair_api.common.support.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -18,8 +20,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.UUID; @Entity @@ -79,14 +79,4 @@ public InterviewQuestion( public void updateStatus(final QuestionStatus status) { this.status = status; } - - public void updateLatestScore(final Integer score) { - this.latestScore = score; - } - - public boolean isTodayQuestionFirstAnswer() { - return questionType == QuestionType.TEXT - && getCreatedAt().toLocalDate().equals(LocalDate.now()) - && latestScore == null; - } } diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionStatus.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionStatus.java new file mode 100644 index 0000000..028c12f --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionStatus.java @@ -0,0 +1,5 @@ +package io.wisoft.prepair.prepair_api.interview.question.entity; + +public enum QuestionStatus { + ANSWERED, UNANSWERED +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionType.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionType.java new file mode 100644 index 0000000..28f489c --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/entity/QuestionType.java @@ -0,0 +1,5 @@ +package io.wisoft.prepair.prepair_api.interview.question.entity; + +public enum QuestionType { + TEXT, VIDEO, COMPANY +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/repository/QuestionRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/repository/QuestionRepository.java new file mode 100644 index 0000000..c6a713f --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/repository/QuestionRepository.java @@ -0,0 +1,36 @@ +package io.wisoft.prepair.prepair_api.interview.question.repository; + +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface QuestionRepository extends JpaRepository { + + List findByMemberId(UUID memberId); + + List findByMemberIdAndQuestionTypeOrderByCreatedAtDesc(UUID memberId, QuestionType questionType); + + Optional findByIdAndMemberId(UUID id, UUID memberId); + + List findByInterviewSessionId(UUID sessionId); + + @Modifying(clearAutomatically = true) + @Query("UPDATE InterviewQuestion q SET q.latestScore = :score " + + "WHERE q.id = :id " + + "AND q.latestScore IS NULL " + + "AND q.questionType = io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType.TEXT " + + "AND q.createdAt >= :startOfDay AND q.createdAt < :endOfDay") + int updateLatestScoreIfFirstTime( + @Param("id") UUID id, + @Param("score") Integer score, + @Param("startOfDay") LocalDateTime startOfDay, + @Param("endOfDay") LocalDateTime endOfDay); +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java similarity index 84% rename from src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionService.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java index fe8e7eb..1c918ac 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java @@ -1,16 +1,15 @@ -package io.wisoft.prepair.prepair_api.scheduler; - -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.Notification; -import io.wisoft.prepair.prepair_api.global.client.member.MemberServiceClient; -import io.wisoft.prepair.prepair_api.global.client.member.dto.MemberSchedulerInfo; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.QuestionWithTags; -import io.wisoft.prepair.prepair_api.notification.email.EmailService; -import io.wisoft.prepair.prepair_api.notification.kakao.KakaoService; -import io.wisoft.prepair.prepair_api.prompt.PromptBuilder; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; -import io.wisoft.prepair.prepair_api.service.question.QuestionPersistService; +package io.wisoft.prepair.prepair_api.interview.question.service; + +import io.wisoft.prepair.prepair_api.external.member.enums.Notification; +import io.wisoft.prepair.prepair_api.external.member.MemberServiceClient; +import io.wisoft.prepair.prepair_api.external.member.dto.MemberSchedulerInfo; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.external.openai.dto.QuestionWithTags; +import io.wisoft.prepair.prepair_api.external.notification.email.EmailService; +import io.wisoft.prepair.prepair_api.external.notification.kakao.KakaoService; +import io.wisoft.prepair.prepair_api.interview.prompt.PromptBuilder; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,10 +23,10 @@ @Slf4j @Service @RequiredArgsConstructor -public class TodayQuestionService { +public class DailyQuestionGenerationService { private final QuestionRepository questionRepository; - private final QuestionPersistService interviewQuestionService; + private final QuestionPersistenceService interviewQuestionService; private final MemberServiceClient memberServiceClient; private final OpenAiClient openAiClient; private final PromptBuilder promptBuilder; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionPersistenceService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionPersistenceService.java new file mode 100644 index 0000000..d90c46f --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionPersistenceService.java @@ -0,0 +1,64 @@ +package io.wisoft.prepair.prepair_api.interview.question.service; + +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; +import io.wisoft.prepair.prepair_api.external.openai.dto.QuestionWithTags; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; + +import java.util.UUID; + +import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class QuestionPersistenceService { + + private final QuestionRepository questionRepository; + private final SessionRepository sessionRepository; + + public InterviewQuestion saveTodayQuestion(UUID memberId, QuestionWithTags result) { + return save(memberId, + QuestionType.TEXT, + null, + result, + null); + } + + public InterviewQuestion saveCompanyQuestion(UUID memberId, QuestionWithTags result, JobPosting jobPosting) { + return save(memberId, + QuestionType.COMPANY, + jobPosting, + result, + null); + } + + public InterviewQuestion saveVideoQuestion(UUID memberId, QuestionWithTags result, InterviewSession session) { + return save(memberId, + QuestionType.VIDEO, + null, + result, + session); + } + + private InterviewQuestion save(UUID memberId, QuestionType questionType, JobPosting jobPosting, QuestionWithTags result, InterviewSession session) { + InterviewQuestion question = new InterviewQuestion( + memberId, + result.question(), + questionType, + result.joinTags(), + jobPosting, + session + ); + return questionRepository.save(question); + } + + public InterviewSession createSession(UUID memberId, int totalQuestionCount) { + return sessionRepository.save(new InterviewSession(memberId, totalQuestionCount)); + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionService.java new file mode 100644 index 0000000..9fe0588 --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionService.java @@ -0,0 +1,90 @@ +package io.wisoft.prepair.prepair_api.interview.question.service; + +import io.wisoft.prepair.prepair_api.interview.jobposting.dto.JobPostingRequest; +import io.wisoft.prepair.prepair_api.interview.question.dto.VideoInterviewRequest; +import io.wisoft.prepair.prepair_api.interview.question.dto.CompanyQuestionResponse; +import io.wisoft.prepair.prepair_api.interview.jobposting.dto.JobPostingResponse; +import io.wisoft.prepair.prepair_api.interview.question.dto.QuestionResponse; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.jobposting.entity.JobPosting; +import io.wisoft.prepair.prepair_api.interview.question.entity.QuestionType; +import io.wisoft.prepair.prepair_api.external.member.MemberServiceClient; +import io.wisoft.prepair.prepair_api.common.exception.BusinessException; +import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; +import io.wisoft.prepair.prepair_api.external.member.dto.MemberSchedulerInfo; +import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; +import io.wisoft.prepair.prepair_api.external.openai.dto.QuestionWithTags; +import io.wisoft.prepair.prepair_api.interview.prompt.PromptBuilder; +import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; + +import io.wisoft.prepair.prepair_api.interview.jobposting.service.JobPostingService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class QuestionService { + + private final QuestionPersistenceService questionPersistService; + private final JobPostingService jobPostingService; + private final QuestionRepository questionRepository; + private final MemberServiceClient memberServiceClient; + private final OpenAiClient openAiClient; + private final PromptBuilder promptBuilder; + + public List getQuestions(UUID memberId, QuestionType type) { + return questionRepository.findByMemberIdAndQuestionTypeOrderByCreatedAtDesc(memberId, type) + .stream() + .map(QuestionResponse::from) + .toList(); + } + + public QuestionResponse getQuestion(UUID questionId, UUID memberId) { + InterviewQuestion question = questionRepository.findByIdAndMemberId(questionId, memberId) + .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); + + return QuestionResponse.from(question); + } + + @Transactional + public CompanyQuestionResponse generateCompanyQuestions(UUID memberId, JobPostingRequest jobPostingRequest) { + JobPosting jobPosting = jobPostingService.crawlAndSave(jobPostingRequest.url()); + String prompt = promptBuilder.buildCompanyQuestionPrompt(jobPosting.getContent()); + + List aiQuestions = openAiClient.generateQuestions(prompt); + + List questionResponses = aiQuestions.stream() + .map(aiQuestion -> questionPersistService.saveCompanyQuestion(memberId, aiQuestion, jobPosting)) + .map(QuestionResponse::from) + .toList(); + + log.info("기업 맞춤 질문 생성 완료 - memberId: {}, jobPostingId: {}", memberId, jobPosting.getId()); + return CompanyQuestionResponse.of(JobPostingResponse.from(jobPosting), questionResponses); + } + + @Transactional + public List generateVideoQuestions(UUID memberId, VideoInterviewRequest request) { + MemberSchedulerInfo member = memberServiceClient.getMember(memberId); + String prompt = promptBuilder.buildVideoQuestionPrompt(member.job(), request.count()); + + List aiQuestions = openAiClient.generateQuestions(prompt); + InterviewSession session = questionPersistService.createSession(memberId, aiQuestions.size()); + + List questionResponses = aiQuestions.stream() + .map(aiQuestion -> questionPersistService.saveVideoQuestion(memberId, aiQuestion, session)) + .map(QuestionResponse::from) + .toList(); + + log.info("화상 면접 질문 생성 완료 - memberId: {}, sessionId: {}", memberId, session.getId()); + return questionResponses; + } +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewSession.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/InterviewSession.java similarity index 85% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewSession.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/InterviewSession.java index b1ce5f2..aee029a 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/InterviewSession.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/InterviewSession.java @@ -1,7 +1,7 @@ -package io.wisoft.prepair.prepair_api.entity; +package io.wisoft.prepair.prepair_api.interview.session.entity; -import io.wisoft.prepair.prepair_api.entity.enums.SessionStatus; -import io.wisoft.prepair.prepair_api.global.common.BaseTimeEntity; +import io.wisoft.prepair.prepair_api.interview.session.entity.SessionStatus; +import io.wisoft.prepair.prepair_api.common.support.BaseTimeEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SessionStatus.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/SessionStatus.java similarity index 53% rename from src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SessionStatus.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/SessionStatus.java index 8e4453b..1296d7d 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/entity/enums/SessionStatus.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/entity/SessionStatus.java @@ -1,4 +1,4 @@ -package io.wisoft.prepair.prepair_api.entity.enums; +package io.wisoft.prepair.prepair_api.interview.session.entity; public enum SessionStatus { IN_PROGRESS, diff --git a/src/main/java/io/wisoft/prepair/prepair_api/repository/SessionRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java similarity index 53% rename from src/main/java/io/wisoft/prepair/prepair_api/repository/SessionRepository.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java index c33a5d6..c925314 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/repository/SessionRepository.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/session/repository/SessionRepository.java @@ -1,6 +1,6 @@ -package io.wisoft.prepair.prepair_api.repository; +package io.wisoft.prepair.prepair_api.interview.session.repository; -import io.wisoft.prepair.prepair_api.entity.InterviewSession; +import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/io/wisoft/prepair/prepair_api/repository/QuestionRepository.java b/src/main/java/io/wisoft/prepair/prepair_api/repository/QuestionRepository.java deleted file mode 100644 index 03f2a8a..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/repository/QuestionRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.wisoft.prepair.prepair_api.repository; - -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public interface QuestionRepository extends JpaRepository { - - List findByMemberId(UUID memberId); - - List findByMemberIdAndQuestionTypeOrderByCreatedAtDesc(UUID memberId, QuestionType questionType); - - Optional findByIdAndMemberId(UUID id, UUID memberId); - - List findByInterviewSessionId(UUID sessionId); -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java index 73305b9..c28bc45 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java @@ -1,5 +1,6 @@ package io.wisoft.prepair.prepair_api.scheduler; +import io.wisoft.prepair.prepair_api.interview.question.service.DailyQuestionGenerationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; @@ -14,16 +15,15 @@ @RequiredArgsConstructor public class TodayQuestionScheduler { - private final TodayQuestionService todayQuestionService; + private final DailyQuestionGenerationService dailyQuestionGenerationService; @Scheduled(cron = "0 0 9 * * *") -// @Scheduled(cron = "0 */5 * * * *") public void generateTodayQuestions() { String correlationId = "SCHEDULER-" + UUID.randomUUID().toString(); MDC.put("correlationId", correlationId); try { log.info("오늘의 질문 생성 스케줄러 시작 - {}", LocalDateTime.now()); - todayQuestionService.generateTodayQuestions(); + dailyQuestionGenerationService.generateTodayQuestions(); log.info("오늘의 질문 생성 스케줄러 종료"); } finally { MDC.remove("correlationId"); diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerPersistService.java b/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerPersistService.java deleted file mode 100644 index a2507cc..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerPersistService.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.wisoft.prepair.prepair_api.service.answer; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.wisoft.prepair.prepair_api.dto.CombinedFeedbackResult; -import io.wisoft.prepair.prepair_api.dto.FeedbackResult; -import io.wisoft.prepair.prepair_api.dto.response.FeedbackDetail; -import io.wisoft.prepair.prepair_api.entity.InterviewAnswer; -import io.wisoft.prepair.prepair_api.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.AnswerType; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionStatus; -import io.wisoft.prepair.prepair_api.global.client.member.MemberServiceClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.repository.AnswerRepository; -import io.wisoft.prepair.prepair_api.repository.FeedbackRepository; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; - -import java.util.UUID; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Slf4j -@RequiredArgsConstructor -public class AnswerPersistService { - - private final QuestionRepository questionRepository; - private final AnswerRepository answerRepository; - private final FeedbackRepository feedbackRepository; - private final MemberServiceClient memberServiceClient; - private final ObjectMapper objectMapper; - - @Transactional - public InterviewFeedback saveAnswerAndFeedback( - final UUID questionId, final UUID memberId, final String answer, - final FeedbackResult result, final FeedbackDetail detail, - final AnswerType answerType, final FeedbackType feedbackType - ) { - InterviewQuestion question = getQuestion(questionId, memberId); - question.updateStatus(QuestionStatus.ANSWERED); - - InterviewAnswer interviewAnswer = answerRepository.save( - new InterviewAnswer(question, answer, answerType, null) - ); - - InterviewFeedback feedback = feedbackRepository.save( - new InterviewFeedback(interviewAnswer, serializeFeedback(detail), feedbackType, result.score()) - ); - - if (question.isTodayQuestionFirstAnswer()) { - memberServiceClient.sendScore(memberId, result.score()); - } - - question.updateLatestScore(result.score()); - - return feedback; - } - - @Transactional - public InterviewAnswer createVideoAnswer(final UUID questionId, final UUID memberId) { - InterviewQuestion question = getQuestion(questionId, memberId); - question.updateStatus(QuestionStatus.ANSWERED); - - return answerRepository.save( - new InterviewAnswer(question, "", AnswerType.VIDEO, null)); - } - - @Transactional - public void updateMediaUrl(final UUID answerId, final String mediaUrl) { - InterviewAnswer interviewAnswer = answerRepository.findById(answerId) - .orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND)); - interviewAnswer.updateMediaUrl(mediaUrl); - } - - @Transactional - public void updateAnswer(final UUID answerId, final String answer) { - InterviewAnswer interviewAnswer = answerRepository.findById(answerId) - .orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND)); - interviewAnswer.updateAnswer(answer); - } - - @Transactional - public void saveFeedback(final UUID answerId, final FeedbackResult result, - final FeedbackDetail detail, final FeedbackType feedbackType) { - InterviewAnswer interviewAnswer = answerRepository.findById(answerId) - .orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND)); - - feedbackRepository.save( - new InterviewFeedback(interviewAnswer, serializeFeedback(detail), feedbackType, result.score())); - } - - @Transactional - public void saveCombinedFeedback(final UUID answerId, final CombinedFeedbackResult result) { - InterviewAnswer interviewAnswer = answerRepository.findById(answerId) - .orElseThrow(() -> new BusinessException(ErrorCode.ANSWER_NOT_FOUND)); - - feedbackRepository.save( - new InterviewFeedback(interviewAnswer, result.combineFeedback(), FeedbackType.COMBINED, result.score())); - } - - private InterviewQuestion getQuestion(UUID questionId, UUID memberId) { - return questionRepository.findByIdAndMemberId(questionId, memberId) - .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); - } - - private String serializeFeedback(final FeedbackDetail detail) { - try { - return objectMapper.writeValueAsString(detail); - } catch (JsonProcessingException e) { - log.error("피드백 직렬화 실패", e); - throw new BusinessException(ErrorCode.INTERNAL_ERROR); - } - } -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerService.java b/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerService.java deleted file mode 100644 index 161458e..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/answer/AnswerService.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.wisoft.prepair.prepair_api.service.answer; - -import io.wisoft.prepair.prepair_api.dto.FeedbackResult; -import io.wisoft.prepair.prepair_api.dto.response.FeedbackDetail; -import io.wisoft.prepair.prepair_api.dto.response.FeedbackResponse; -import io.wisoft.prepair.prepair_api.entity.InterviewAnswer; -import io.wisoft.prepair.prepair_api.entity.InterviewFeedback; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.enums.AnswerType; -import io.wisoft.prepair.prepair_api.entity.enums.FeedbackType; -import io.wisoft.prepair.prepair_api.global.client.member.MemberServiceClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; - -import io.wisoft.prepair.prepair_api.service.answer.event.AnalysisCompletionTracker; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -@Slf4j -@Service -@RequiredArgsConstructor -public class AnswerService { - - private final AnswerPersistService answerPersistService; - private final VideoAnswerAnalyzer videoAnswerAnalyzer; - private final FeedbackGenerator feedbackGenerator; - private final QuestionRepository questionRepository; - private final MemberServiceClient memberServiceClient; - private final AnalysisCompletionTracker completionTracker; - - public FeedbackResponse submitAnswer(final UUID questionId, final UUID memberId, final String answer) { - InterviewQuestion question = questionRepository.findByIdAndMemberId(questionId, memberId) - .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); - - FeedbackResult result = feedbackGenerator.generate(question, answer); - FeedbackDetail detail = new FeedbackDetail(result.good(), result.improvement(), result.recommendation()); - - InterviewFeedback feedback = answerPersistService.saveAnswerAndFeedback( - questionId, memberId, answer, result, detail, AnswerType.TEXT, FeedbackType.TEXT); - - log.info("답변 제출 및 피드백 생성 완료 - questionId: {}, score: {}", questionId, result.score()); - return FeedbackResponse.from(feedback, detail); - } - - public void submitVideoAnswer(final UUID questionId, final UUID memberId, final MultipartFile video) { - InterviewQuestion question = questionRepository.findByIdAndMemberId(questionId, memberId) - .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); - - String email = memberServiceClient.getMember(memberId).email(); - InterviewAnswer answer = answerPersistService.createVideoAnswer(questionId, memberId); - - Path videoPath = createTempFile(video); - - completionTracker.init(answer.getId(), videoPath); - - videoAnswerAnalyzer.uploadToS3(answer.getId(), videoPath, video.getContentType(), email); - videoAnswerAnalyzer.analyzeSTT(answer.getId(), questionId, memberId, videoPath, question.getQuestionTag()); - videoAnswerAnalyzer.analyzeVideo(answer.getId(), videoPath); - } - - private Path createTempFile(MultipartFile video) { - try { - Path videoPath = Files.createTempFile("video-", getExtension(video.getOriginalFilename())); - video.transferTo(videoPath); - return videoPath; - } catch (Exception e) { - log.error("영상 임시 파일 생성 실패 - filename: {}", video.getOriginalFilename(), e); - throw new BusinessException(ErrorCode.INTERNAL_ERROR); - } - } - - private String getExtension(String filename) { - if (filename != null && filename.contains(".")) { - return filename.substring(filename.lastIndexOf(".")); - } - return ".webm"; - } -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionPersistService.java b/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionPersistService.java deleted file mode 100644 index dac5424..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionPersistService.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.wisoft.prepair.prepair_api.service.question; - -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.InterviewSession; -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.QuestionWithTags; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; - -import java.util.UUID; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class QuestionPersistService { - - private final QuestionRepository questionRepository; - - public InterviewQuestion saveTodayQuestion(UUID memberId, QuestionWithTags result) { - return save(memberId, QuestionType.TEXT, null, result, null); - } - - public InterviewQuestion saveCompanyQuestion(UUID memberId, QuestionWithTags result, JobPosting jobPosting) { - return save(memberId, QuestionType.COMPANY, jobPosting, result, null); - } - - public InterviewQuestion saveVideoQuestion(UUID memberId, QuestionWithTags result, InterviewSession session) { - return save(memberId, QuestionType.VIDEO, null, result, session); - } - - public InterviewQuestion save(UUID memberId, QuestionType questionType, JobPosting jobPosting, - QuestionWithTags result, InterviewSession session) { - InterviewQuestion question = new InterviewQuestion( - memberId, - result.question(), - questionType, - result.joinTags(), - jobPosting, - session - ); - return questionRepository.save(question); - } -} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionService.java b/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionService.java deleted file mode 100644 index a459d8a..0000000 --- a/src/main/java/io/wisoft/prepair/prepair_api/service/question/QuestionService.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.wisoft.prepair.prepair_api.service.question; - -import io.wisoft.prepair.prepair_api.dto.request.VideoInterviewRequest; -import io.wisoft.prepair.prepair_api.entity.InterviewQuestion; -import io.wisoft.prepair.prepair_api.entity.InterviewSession; -import io.wisoft.prepair.prepair_api.entity.JobPosting; -import io.wisoft.prepair.prepair_api.entity.enums.QuestionType; -import io.wisoft.prepair.prepair_api.global.client.member.MemberServiceClient; -import io.wisoft.prepair.prepair_api.global.exception.BusinessException; -import io.wisoft.prepair.prepair_api.global.exception.ErrorCode; -import io.wisoft.prepair.prepair_api.global.client.member.dto.MemberSchedulerInfo; -import io.wisoft.prepair.prepair_api.global.client.openai.OpenAiClient; -import io.wisoft.prepair.prepair_api.global.client.openai.dto.QuestionWithTags; -import io.wisoft.prepair.prepair_api.prompt.PromptBuilder; -import io.wisoft.prepair.prepair_api.repository.QuestionRepository; -import io.wisoft.prepair.prepair_api.repository.SessionRepository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.UUID; - - -@Slf4j -@Service -@RequiredArgsConstructor -public class QuestionService { - - private final QuestionRepository questionRepository; - private final SessionRepository sessionRepository; - private final QuestionPersistService interviewQuestionService; - private final MemberServiceClient memberServiceClient; - private final OpenAiClient openAiClient; - private final PromptBuilder promptBuilder; - - @Transactional(readOnly = true) - public List getQuestions(UUID memberId, QuestionType type) { - return questionRepository.findByMemberIdAndQuestionTypeOrderByCreatedAtDesc(memberId, type); - } - - @Transactional(readOnly = true) - public InterviewQuestion getQuestion(UUID questionId, UUID memberId) { - return questionRepository.findByIdAndMemberId(questionId, memberId) - .orElseThrow(() -> new BusinessException(ErrorCode.QUESTION_NOT_FOUND)); - } - - public List generateCompanyQuestions(UUID memberId, JobPosting jobPosting) { - try { - final String prompt = promptBuilder.buildCompanyQuestionPrompt(jobPosting.getContent()); - final List results = openAiClient.generateQuestions(prompt); - final List questions = results.stream() - .map(result -> - interviewQuestionService.saveCompanyQuestion(memberId, result, jobPosting)) - .toList(); - log.info("기업 맞춤 질문 생성 완료 - memberId: {}, jobPostingId: {}", memberId, jobPosting.getId()); - - return questions; - } catch (Exception e) { - log.error("기업 맞춤 질문 생성 실패 - memberId: {}, jobPostingId: {}", memberId, jobPosting.getId(), e); - throw e; - } - } - - public List generateVideoQuestions(UUID memberId, VideoInterviewRequest request) { - MemberSchedulerInfo member = memberServiceClient.getMember(memberId); - String prompt = promptBuilder.buildVideoQuestionPrompt(member.job(), request.count()); - List results = openAiClient.generateQuestions(prompt); - - InterviewSession session = sessionRepository.save(new InterviewSession(memberId, request.count())); - - List questions = results.stream() - .map(result -> interviewQuestionService.saveVideoQuestion(memberId, result, session)) - .toList(); - - log.info("화상 면접 질문 생성 완료 - memberId: {}, sessionId: {}", memberId, session.getId()); - return questions; - } - - public void validateSessionOwner(UUID sessionId, UUID memberId) { - InterviewSession session = sessionRepository.findById(sessionId) - .orElseThrow(() -> new BusinessException(ErrorCode.SESSION_NOT_FOUND)); - if (!session.getMemberId().equals(memberId)) { - throw new BusinessException(ErrorCode.FORBIDDEN); - } - } -}