diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/company/dto/CompanyDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/company/dto/CompanyDto.java index f8a6f2fea..ee37645f9 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/company/dto/CompanyDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/company/dto/CompanyDto.java @@ -1,8 +1,9 @@ package com.shyashyashya.refit.domain.company.dto; import com.shyashyashya.refit.domain.company.model.Company; +import jakarta.validation.constraints.NotNull; -public record CompanyDto(Long companyId, String companyName, String companyLogoUrl) { +public record CompanyDto(@NotNull Long companyId, @NotNull String companyName, String companyLogoUrl) { public static CompanyDto from(Company company) { return new CompanyDto(company.getId(), company.getName(), company.getLogoUrl()); } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/industry/dto/IndustryResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/industry/dto/IndustryResponse.java index 3c9792840..70c3ae9fd 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/industry/dto/IndustryResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/industry/dto/IndustryResponse.java @@ -1,8 +1,10 @@ package com.shyashyashya.refit.domain.industry.dto; import com.shyashyashya.refit.domain.industry.model.Industry; +import jakarta.validation.constraints.NotNull; -public record IndustryResponse(Long industryId, String industryName) { +public record IndustryResponse( + @NotNull Long industryId, @NotNull String industryName) { public static IndustryResponse from(Industry industry) { return new IndustryResponse(industry.getId(), industry.getName()); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/DashboardController.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/DashboardController.java index d4b678855..6aafbee91 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/DashboardController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/DashboardController.java @@ -16,6 +16,7 @@ import jakarta.validation.constraints.Positive; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -68,7 +69,7 @@ public ResponseEntity>> getDashboard """) @GetMapping("/interview/upcoming") public ResponseEntity>> getUpcomingInterviews( - Pageable pageable) { + @ParameterObject Pageable pageable) { var body = dashboardService.getUpcomingInterviews(pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -77,7 +78,7 @@ public ResponseEntity>> get @Operation(summary = "대시보드에서 '내가 어렵게 느낀 질문'을 조회합니다.") @GetMapping("/qna-set/my/difficult") public ResponseEntity>> getMyDifficultQnaSets( - Pageable pageable) { + @ParameterObject Pageable pageable) { var body = dashboardService.getMyDifficultQnaSets(pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -86,7 +87,7 @@ public ResponseEntity>> g @Operation(summary = "대시보드에서 복기 대기중인 면접 리스트를 조회합니다.") @GetMapping("/interview/debrief-uncompleted") public ResponseEntity>> - getDebriefIncompletedInterviews(Pageable pageable) { + getDebriefIncompletedInterviews(@ParameterObject Pageable pageable) { var body = dashboardService.getDebriefIncompletedInterviews(pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/InterviewMyController.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/InterviewMyController.java index 823d42a3a..7daac1826 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/InterviewMyController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/api/InterviewMyController.java @@ -11,7 +11,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -35,7 +37,7 @@ public class InterviewMyController { """) @PostMapping("/search") public ResponseEntity>> searchInterviews( - @Valid @RequestBody InterviewSearchRequest request, Pageable pageable) { + @Valid @RequestBody InterviewSearchRequest request, @ParameterObject Pageable pageable) { var body = interviewService.searchMyInterviews(request, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -47,9 +49,20 @@ public ResponseEntity>> searchInterviews( """) @GetMapping("/draft") public ResponseEntity>> getMyInterviewDrafts( - @RequestParam InterviewDraftType interviewDraftType, Pageable pageable) { + @RequestParam InterviewDraftType interviewDraftType, @ParameterObject Pageable pageable) { var body = interviewService.getMyInterviewDrafts(interviewDraftType, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); } + + @Operation(summary = "아직 기록하지 않은 나의 면접들을 조회합니다.", description = """ + 현재일 기준 최근 한 달동안 본 면접 데이터 중 상태가 '기록전' 상태인 면접을 면접일 기준 내림차순으로 조회합니다. + 모바일 화면에서 기록할 면접을 조회할 때 사용됩니다. + """) + @GetMapping("/not-logged") + public ResponseEntity>> getMyNotLoggedInterviews() { + var body = interviewService.getMyNotLoggedInterviews(); + var response = ApiResponse.success(COMMON200, body); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewDto.java index ab8dfd932..a41ace525 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewDto.java @@ -3,21 +3,24 @@ import com.shyashyashya.refit.domain.company.model.Company; import com.shyashyashya.refit.domain.interview.model.Interview; import com.shyashyashya.refit.domain.interview.model.InterviewResultStatus; +import com.shyashyashya.refit.domain.interview.model.InterviewReviewStatus; import com.shyashyashya.refit.domain.interview.model.InterviewType; import com.shyashyashya.refit.domain.jobcategory.model.JobCategory; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; public record InterviewDto( - Long interviewId, - InterviewType interviewType, - LocalDateTime interviewStartAt, - InterviewResultStatus interviewResultStatus, + @NotNull Long interviewId, + @NotNull InterviewType interviewType, + @NotNull LocalDateTime interviewStartAt, + @NotNull InterviewResultStatus interviewResultStatus, + @NotNull InterviewReviewStatus interviewReviewStatus, String interviewRawText, - String companyName, - Long jobCategoryId, - String jobCategoryName, - LocalDateTime updatedAt, - LocalDateTime createdAt) { + @NotNull String companyName, + @NotNull Long jobCategoryId, + @NotNull String jobCategoryName, + @NotNull LocalDateTime updatedAt, + @NotNull LocalDateTime createdAt) { public static InterviewDto from(Interview interview) { Company company = interview.getCompany(); JobCategory jobCategory = interview.getJobCategory(); @@ -27,6 +30,7 @@ public static InterviewDto from(Interview interview) { interview.getInterviewType(), interview.getStartAt(), interview.getResultStatus(), + interview.getReviewStatus(), interview.getRawText(), company.getName(), jobCategory.getId(), diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewFullDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewFullDto.java index 95ef1ce36..62dcc621f 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewFullDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewFullDto.java @@ -6,6 +6,7 @@ import com.shyashyashya.refit.domain.interview.model.InterviewType; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; import com.shyashyashya.refit.domain.qnaset.model.QnaSetSelfReview; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -14,17 +15,17 @@ @Builder(access = AccessLevel.PRIVATE) public record InterviewFullDto( - Long interviewId, - InterviewType interviewType, - LocalDateTime interviewStartAt, - InterviewResultStatus interviewResultStatus, - String company, - Long industryId, - Long jobCategoryId, + @NotNull Long interviewId, + @NotNull InterviewType interviewType, + @NotNull LocalDateTime interviewStartAt, + @NotNull InterviewResultStatus interviewResultStatus, + @NotNull String company, + @NotNull Long industryId, + @NotNull Long jobCategoryId, String jobRole, - LocalDateTime updatedAt, + @NotNull LocalDateTime updatedAt, String pdfUrl, - List qnaSets, + @NotNull List qnaSets, InterviewSelfReviewDto interviewSelfReview) { public static InterviewFullDto fromInterviewWithEmptyQnaSets(Interview interview) { return InterviewFullDto.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSelfReviewDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSelfReviewDto.java index 2cb4d4754..2fdb6e078 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSelfReviewDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSelfReviewDto.java @@ -1,8 +1,12 @@ package com.shyashyashya.refit.domain.interview.dto; import com.shyashyashya.refit.domain.interview.model.InterviewSelfReview; +import jakarta.validation.constraints.NotNull; -public record InterviewSelfReviewDto(String keepText, String problemText, String tryText) { +public record InterviewSelfReviewDto( + @NotNull String keepText, + @NotNull String problemText, + @NotNull String tryText) { public static InterviewSelfReviewDto from(InterviewSelfReview interviewSelfReview) { return new InterviewSelfReviewDto( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSimpleDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSimpleDto.java index 2db8f60f0..f173ea27a 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSimpleDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/InterviewSimpleDto.java @@ -4,16 +4,17 @@ import com.shyashyashya.refit.domain.interview.model.Interview; import com.shyashyashya.refit.domain.interview.model.InterviewReviewStatus; import com.shyashyashya.refit.domain.interview.model.InterviewType; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; public record InterviewSimpleDto( - Long interviewId, - InterviewType interviewType, - InterviewReviewStatus interviewReviewStatus, - LocalDateTime interviewStartAt, - CompanyDto companyInfo, - String jobCategoryName, - LocalDateTime updatedAt) { + @NotNull Long interviewId, + @NotNull InterviewType interviewType, + @NotNull InterviewReviewStatus interviewReviewStatus, + @NotNull LocalDateTime interviewStartAt, + @NotNull CompanyDto companyInfo, + @NotNull String jobCategoryName, + @NotNull LocalDateTime updatedAt) { public static InterviewSimpleDto from(Interview interview) { return new InterviewSimpleDto( interview.getId(), diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/QnaSetDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/QnaSetDto.java index 87b3f7bdb..eefc41f34 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/QnaSetDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/QnaSetDto.java @@ -2,15 +2,16 @@ import com.shyashyashya.refit.domain.qnaset.model.QnaSet; import com.shyashyashya.refit.domain.qnaset.model.QnaSetSelfReview; +import jakarta.validation.constraints.NotNull; public record QnaSetDto( - Long qnaSetId, - Long interviewId, - String questionText, - String answerText, - String qnaSetSelfReviewText, + @NotNull Long qnaSetId, + @NotNull Long interviewId, + @NotNull String questionText, + @NotNull String answerText, + @NotNull String qnaSetSelfReviewText, StarAnalysisDto starAnalysis, - Boolean isMarkedDifficult) { + @NotNull Boolean isMarkedDifficult) { public static QnaSetDto from(QnaSet qnaSet, QnaSetSelfReview qnaSetSelfReview, StarAnalysisDto starAnalysisDto) { String selfReviewText = ""; if (qnaSetSelfReview != null) { diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/StarAnalysisDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/StarAnalysisDto.java index 6a2284faf..e3c3d635b 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/StarAnalysisDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/StarAnalysisDto.java @@ -2,13 +2,14 @@ import com.shyashyashya.refit.domain.qnaset.model.StarAnalysis; import com.shyashyashya.refit.domain.qnaset.model.StarInclusionLevel; +import jakarta.validation.constraints.NotNull; public record StarAnalysisDto( - StarInclusionLevel sInclusionLevel, - StarInclusionLevel tInclusionLevel, - StarInclusionLevel aInclusionLevel, - StarInclusionLevel rInclusionLevel, - String overallSummary) { + @NotNull StarInclusionLevel sInclusionLevel, + @NotNull StarInclusionLevel tInclusionLevel, + @NotNull StarInclusionLevel aInclusionLevel, + @NotNull StarInclusionLevel rInclusionLevel, + @NotNull String overallSummary) { public static StarAnalysisDto from(StarAnalysis starAnalysis) { return new StarAnalysisDto( starAnalysis.getSInclusionLevel(), diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardCalendarResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardCalendarResponse.java index 03b14b2c8..bc89ad4ed 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardCalendarResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardCalendarResponse.java @@ -2,10 +2,14 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.interview.model.Interview; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.util.List; -public record DashboardCalendarResponse(LocalDate date, Long dDay, List interviews) { +public record DashboardCalendarResponse( + @NotNull LocalDate date, + @NotNull Long dDay, + @NotNull List interviews) { public static DashboardCalendarResponse of(LocalDate date, Long dDay, List interviews) { return new DashboardCalendarResponse( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardDebriefIncompletedInterviewResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardDebriefIncompletedInterviewResponse.java index c70414d57..602cfe513 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardDebriefIncompletedInterviewResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardDebriefIncompletedInterviewResponse.java @@ -2,8 +2,10 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.interview.model.Interview; +import jakarta.validation.constraints.NotNull; -public record DashboardDebriefIncompletedInterviewResponse(InterviewDto interview, Long passedDays) { +public record DashboardDebriefIncompletedInterviewResponse( + @NotNull InterviewDto interview, @NotNull Long passedDays) { public static DashboardDebriefIncompletedInterviewResponse of(Interview interview, Long passedDays) { return new DashboardDebriefIncompletedInterviewResponse(InterviewDto.from(interview), passedDays); } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardHeadlineResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardHeadlineResponse.java index 6cb61c9dd..df4e7e53c 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardHeadlineResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardHeadlineResponse.java @@ -1,9 +1,12 @@ package com.shyashyashya.refit.domain.interview.dto.response; import com.shyashyashya.refit.domain.user.model.User; +import jakarta.validation.constraints.NotNull; public record DashboardHeadlineResponse( - DashboardHeadlineType headlineType, String nickname, Long upcomingInterviewDday) { + @NotNull DashboardHeadlineType headlineType, + @NotNull String nickname, + Long upcomingInterviewDday) { public static DashboardHeadlineResponse registerInterview(User user) { return new DashboardHeadlineResponse(DashboardHeadlineType.REGISTER_INTERVIEW, user.getNickname(), null); } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardMyDifficultQuestionResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardMyDifficultQuestionResponse.java index 23b615db3..9726c73f2 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardMyDifficultQuestionResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardMyDifficultQuestionResponse.java @@ -2,8 +2,10 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; -public record DashboardMyDifficultQuestionResponse(String question, InterviewDto interview) { +public record DashboardMyDifficultQuestionResponse( + @NotNull String question, @NotNull InterviewDto interview) { public static DashboardMyDifficultQuestionResponse from(QnaSet qnaSet) { return new DashboardMyDifficultQuestionResponse( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardUpcomingInterviewResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardUpcomingInterviewResponse.java index e628a62b2..77078918f 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardUpcomingInterviewResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/DashboardUpcomingInterviewResponse.java @@ -3,10 +3,13 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.interview.model.Interview; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; import java.util.List; public record DashboardUpcomingInterviewResponse( - InterviewDto upcomingInterview, List frequentlyAskedQuestions, List relatedInterviews) { + @NotNull InterviewDto upcomingInterview, + @NotNull List frequentlyAskedQuestions, + @NotNull List relatedInterviews) { public static DashboardUpcomingInterviewResponse of( Interview upcomingInterview, List frequentlyAskedQuestions, List relatedInterviews) { diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/GuideQuestionResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/GuideQuestionResponse.java index 0a7f1ae2a..635412bd0 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/GuideQuestionResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/GuideQuestionResponse.java @@ -1,3 +1,5 @@ package com.shyashyashya.refit.domain.interview.dto.response; -public record GuideQuestionResponse(String guideQuestion) {} +import jakarta.validation.constraints.NotNull; + +public record GuideQuestionResponse(@NotNull String guideQuestion) {} diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/QnaSetCreateResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/QnaSetCreateResponse.java index b1ba6c165..9a04bed94 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/QnaSetCreateResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/dto/response/QnaSetCreateResponse.java @@ -1,12 +1,13 @@ package com.shyashyashya.refit.domain.interview.dto.response; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record QnaSetCreateResponse(Long qnaSetId) { +public record QnaSetCreateResponse(@NotNull Long qnaSetId) { public static QnaSetCreateResponse from(QnaSet qnaSet) { - return new QnaSetCreateResponse(qnaSet.getId()); + return QnaSetCreateResponse.builder().qnaSetId(qnaSet.getId()).build(); } } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/model/InterviewReviewStatus.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/model/InterviewReviewStatus.java index 99e8fa64a..fdf700862 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/model/InterviewReviewStatus.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/model/InterviewReviewStatus.java @@ -3,7 +3,7 @@ public enum InterviewReviewStatus { NOT_LOGGED, // 기록 전 LOG_DRAFT, // 기록 중 - QNA_SET_DRAFT, + QNA_SET_DRAFT, // 질답 세트 검토 중 SELF_REVIEW_DRAFT, // 회고 중 DEBRIEF_COMPLETED // 회고 완료 } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/InterviewCustomRepository.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/InterviewCustomRepository.java index 9e0d516d6..3d314cb48 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/InterviewCustomRepository.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/InterviewCustomRepository.java @@ -5,6 +5,8 @@ import com.shyashyashya.refit.domain.interview.model.InterviewType; import com.shyashyashya.refit.domain.user.model.User; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -19,4 +21,6 @@ Page searchInterviews( LocalDate startDate, LocalDate endDate, Pageable pageable); + + List findInterviewsNotLoggedRecentOneMonth(User user, LocalDateTime now); } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/impl/InterviewCustomRepositoryImpl.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/impl/InterviewCustomRepositoryImpl.java index be2f4232b..030e48e72 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/impl/InterviewCustomRepositoryImpl.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/repository/impl/InterviewCustomRepositoryImpl.java @@ -11,6 +11,7 @@ import com.shyashyashya.refit.domain.interview.repository.InterviewCustomRepository; import com.shyashyashya.refit.domain.user.model.User; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; @@ -59,6 +60,18 @@ public Page searchInterviews( return new PageImpl<>(interviews, pageable, totalSize); } + @Override + public List findInterviewsNotLoggedRecentOneMonth(User user, LocalDateTime now) { + return jpaQueryFactory + .selectFrom(interview) + .where( + interview.user.eq(user), + interview.reviewStatus.eq(InterviewReviewStatus.NOT_LOGGED), + interview.startAt.between(now.minusMonths(1), now)) + .orderBy(interview.startAt.desc()) + .fetch(); + } + private BooleanExpression companyNameContains(String keyword) { if (keyword == null || keyword.isEmpty()) { return null; diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/interview/service/InterviewService.java b/backend/src/main/java/com/shyashyashya/refit/domain/interview/service/InterviewService.java index b8905464c..f91beecf7 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/interview/service/InterviewService.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/interview/service/InterviewService.java @@ -36,6 +36,7 @@ import com.shyashyashya.refit.domain.user.model.User; import com.shyashyashya.refit.global.exception.CustomException; import com.shyashyashya.refit.global.util.RequestUserContext; +import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -249,4 +250,13 @@ private Company findOrSaveCompany(InterviewCreateRequest request) { } }); } + + public List getMyNotLoggedInterviews() { + User requestUser = requestUserContext.getRequestUser(); + + LocalDateTime now = LocalDateTime.now(); + return interviewRepository.findInterviewsNotLoggedRecentOneMonth(requestUser, now).stream() + .map(InterviewSimpleDto::from) + .toList(); + } } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/jobcategory/dto/response/JobCategoryResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/jobcategory/dto/response/JobCategoryResponse.java index 38f5f9a3f..914f3ed00 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/jobcategory/dto/response/JobCategoryResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/jobcategory/dto/response/JobCategoryResponse.java @@ -1,8 +1,10 @@ package com.shyashyashya.refit.domain.jobcategory.dto.response; import com.shyashyashya.refit.domain.jobcategory.model.JobCategory; +import jakarta.validation.constraints.NotNull; -public record JobCategoryResponse(Long jobCategoryId, String jobCategoryName) { +public record JobCategoryResponse( + @NotNull Long jobCategoryId, @NotNull String jobCategoryName) { public static JobCategoryResponse from(JobCategory jobCategory) { return new JobCategoryResponse(jobCategory.getId(), jobCategory.getName()); } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetController.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetController.java index cab52394e..b4a4c721e 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetController.java @@ -19,6 +19,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -42,6 +43,7 @@ public class QnaSetController { private final QnaSetService qnaSetService; private final StarAnalysisAsyncService starAnalysisAsyncService; + // TODO 통합 테스트 작성 @Operation( summary = "지정한 산업군 / 직무의 빈출 질문 답변 세트를 조회합니다.", description = "지정한 산업군 / 직무의 빈출 질문 답변 세트를 조회합니다. 지정하지 않은 필드에 대해서는 전체를 대상으로 조회합니다.") @@ -49,7 +51,7 @@ public class QnaSetController { public ResponseEntity>> getFrequentQuestions( @RequestParam(required = false) Set industryIds, @RequestParam(required = false) Set jobCategoryIds, - Pageable pageable) { + @ParameterObject Pageable pageable) { var body = qnaSetService.getFrequentQuestions(industryIds, jobCategoryIds, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -71,17 +73,16 @@ public ResponseEntity> unmarkDifficultQuestion(@PathVariable L return ResponseEntity.ok(response); } - @Operation(summary = "지정한 질문 답변 세트의 질문 답변 내용을 수정합니다.", description = "질문 답변 내용 수정은 '기록중' 상태에서만 가능합니다.") + @Operation(summary = "지정한 질문 답변 세트의 질문 답변 내용을 수정합니다.", description = "질문 답변 내용 수정은 질답 세트 검토 중 상태에서만 가능합니다.") @PutMapping("/{qnaSetId}") public ResponseEntity> updateQnaSet( @PathVariable Long qnaSetId, @Valid @RequestBody QnaSetUpdateRequest request) { - // TODO : 질문 답변 내용 수정이 '기록중' 상태에서만 가능하도록 검증 추가 qnaSetService.updateQnaSet(qnaSetId, request); var response = ApiResponse.success(COMMON200); return ResponseEntity.ok(response); } - @Operation(summary = "지정한 질문 답변 세트를 삭제합니다.", description = "질문 답변 세트 삭제는 'QnaSetDraft' 상태에서만 가능합니다.") + @Operation(summary = "지정한 질문 답변 세트를 삭제합니다.", description = "질문 답변 세트 삭제는 질답 세트 검토 중 상태에서만 가능합니다.") @DeleteMapping("/{qnaSetId}") public ResponseEntity> deleteQnaSet(@PathVariable Long qnaSetId) { qnaSetService.deleteQnaSet(qnaSetId); @@ -89,7 +90,9 @@ public ResponseEntity> deleteQnaSet(@PathVariable Long qnaSetI return ResponseEntity.ok(resposne); } - @Operation(summary = "지정한 질문 답변 세트에 대해 PDF 하이라이팅 정보를 등록/수정합니다.") + @Operation( + summary = "지정한 질문 답변 세트에 대해 PDF 하이라이팅 정보를 등록/수정합니다.", + description = "PDF 하이라이팅 정보 등록/수정은 질답 세트 검토 중 상태에서만 가능합니다.") @PutMapping("/{qnaSetId}/pdf-highlightings") public ResponseEntity> updatePdfHighlighting( @PathVariable Long qnaSetId, @Valid @RequestBody List request) { @@ -106,6 +109,7 @@ public ResponseEntity>> getPdfHighlightings return ResponseEntity.ok(response); } + // TODO 통합 테스트 작성 (E2E 테스트는 일단 보류) @Operation(summary = "지정한 질문 답변 세트에 대해 스타 분석 생성을 요청합니다.", description = "Gemini 요청을 수행하고 10~20초 뒤에 응답이 반환됩니다.") @PostMapping("/{qnaSetId}/star-analysis") public CompletableFuture>> createStarAnalysis( @@ -115,10 +119,11 @@ public CompletableFuture>> createSta .thenApply(rsp -> ResponseEntity.ok(ApiResponse.success(COMMON200, rsp))); } + // TODO 통합 테스트 작성 @Operation(summary = "지정한 질문 답변 세트가 스크랩 폴더에 포함되어 있는 지 여부가 포함된 스크랩 폴더 리스트를 조회합니다.") @GetMapping("/{qnaSetId}/scrap-folder") public ResponseEntity>> getScrapFoldersContainingQnaSet( - @PathVariable Long qnaSetId, Pageable pageable) { + @PathVariable Long qnaSetId, @ParameterObject Pageable pageable) { var body = qnaSetService.getMyScrapFoldersWithQnaSetContainingInfo(qnaSetId, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetMyController.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetMyController.java index 6943acd73..e7749be55 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetMyController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/api/QnaSetMyController.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -35,7 +36,7 @@ public class QnaSetMyController { description = "나의 빈출 질문 카테고리 리스트와 각 카테고리 별 질문 개수를 질문 개수가 많은 카테고리 순으로 정렬하여 조회합니다.") @GetMapping("/frequent/category") public ResponseEntity>> getMyFrequentQnaSetCategories( - Pageable pageable) { + @ParameterObject Pageable pageable) { var body = qnaSetMyService.getFrequentQnaSetCategories(pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -44,7 +45,7 @@ public ResponseEntity>> getMyFr @Operation(summary = "나의 빈출 질문 중 특정 카테고리에 속한 질문들을 조회합니다.", description = "나의 빈출 질문 중 특정 카테고리에 속한 질문들을 조회합니다.") @GetMapping("/frequent/category/{categoryId}") public ResponseEntity>> - getMyFrequentQnaSetCategoryQuestions(@PathVariable Long categoryId, Pageable pageable) { + getMyFrequentQnaSetCategoryQuestions(@PathVariable Long categoryId, @ParameterObject Pageable pageable) { var body = qnaSetMyService.getFrequentQnaSetCategoryQuestions(categoryId, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -53,7 +54,7 @@ public ResponseEntity>> getMyFr @Operation(summary = "나의 면접 질문들을 검색합니다.", description = "나의 면접 질문들을 검색합니다. 조건을 넣지 않으면 전체 데이터를 조회합니다.") @PostMapping("/search") public ResponseEntity>> searchMyQnaSet( - @Valid @RequestBody QnaSetSearchRequest request, Pageable pageable) { + @Valid @RequestBody QnaSetSearchRequest request, @ParameterObject Pageable pageable) { var body = qnaSetMyService.searchQnaSets(request, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingDto.java index 5d26cc5a1..a1b16ed5f 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingDto.java @@ -2,9 +2,13 @@ import com.shyashyashya.refit.domain.qnaset.model.PdfHighlighting; import com.shyashyashya.refit.domain.qnaset.model.PdfHighlightingRect; +import jakarta.validation.constraints.NotNull; import java.util.List; -public record PdfHighlightingDto(Long pdfHighlightingId, String highlightingText, List rects) { +public record PdfHighlightingDto( + @NotNull Long pdfHighlightingId, + @NotNull String highlightingText, + @NotNull List rects) { public static PdfHighlightingDto of(PdfHighlighting pdfHighlighting, List rects) { List pdfHighlightingRectDtos = diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingRectDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingRectDto.java index c93252785..d035d16b4 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingRectDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/PdfHighlightingRectDto.java @@ -1,8 +1,14 @@ package com.shyashyashya.refit.domain.qnaset.dto; import com.shyashyashya.refit.domain.qnaset.model.PdfHighlightingRect; +import jakarta.validation.constraints.NotNull; -public record PdfHighlightingRectDto(Double x, Double y, Double width, Double height, Integer pageNumber) { +public record PdfHighlightingRectDto( + @NotNull Double x, + @NotNull Double y, + @NotNull Double width, + @NotNull Double height, + @NotNull Integer pageNumber) { public static PdfHighlightingRectDto from(PdfHighlightingRect pdfHighlightingRect) { return new PdfHighlightingRectDto( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/QnaSetSimpleDto.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/QnaSetSimpleDto.java index 05dd46cde..bfda37a12 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/QnaSetSimpleDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/QnaSetSimpleDto.java @@ -1,8 +1,12 @@ package com.shyashyashya.refit.domain.qnaset.dto; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; -public record QnaSetSimpleDto(Long qnaSetId, String questionText, String answerText) { +public record QnaSetSimpleDto( + @NotNull Long qnaSetId, + @NotNull String questionText, + @NotNull String answerText) { public static QnaSetSimpleDto from(QnaSet qnaSet) { return new QnaSetSimpleDto(qnaSet.getId(), qnaSet.getQuestionText(), qnaSet.getAnswerText()); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryQuestionResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryQuestionResponse.java index fe70c2baf..c3ca33d8e 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryQuestionResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryQuestionResponse.java @@ -2,8 +2,10 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewSimpleDto; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; -public record FrequentQnaSetCategoryQuestionResponse(String question, InterviewSimpleDto interviewInfo) { +public record FrequentQnaSetCategoryQuestionResponse( + @NotNull String question, @NotNull InterviewSimpleDto interviewInfo) { public static FrequentQnaSetCategoryQuestionResponse from(QnaSet qnaSet) { return new FrequentQnaSetCategoryQuestionResponse( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryResponse.java index 310833f96..b8b8c868d 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetCategoryResponse.java @@ -1,9 +1,13 @@ package com.shyashyashya.refit.domain.qnaset.dto.response; import com.shyashyashya.refit.domain.qnaset.model.QnaSetCategory; +import jakarta.validation.constraints.NotNull; public record FrequentQnaSetCategoryResponse( - Long categoryId, String categoryName, Long frequentCount, Double cohesion) { + @NotNull Long categoryId, + @NotNull String categoryName, + @NotNull Long frequentCount, + @NotNull Double cohesion) { public static FrequentQnaSetCategoryResponse of(QnaSetCategory category, Long frequentCount) { return new FrequentQnaSetCategoryResponse( diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetResponse.java index a850620c2..d08f67756 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/FrequentQnaSetResponse.java @@ -3,17 +3,18 @@ import com.shyashyashya.refit.domain.interview.model.Interview; import com.shyashyashya.refit.domain.interview.model.InterviewType; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) public record FrequentQnaSetResponse( - String industryName, - String jobCategoryName, - InterviewType interviewType, - LocalDateTime interviewStartAt, - String question) { + @NotNull String industryName, + @NotNull String jobCategoryName, + @NotNull InterviewType interviewType, + @NotNull LocalDateTime interviewStartAt, + @NotNull String question) { public static FrequentQnaSetResponse from(QnaSet qnaSet) { // TODO : 질문에서 민감 정보 삭제 Interview interview = qnaSet.getInterview(); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetScrapFolderResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetScrapFolderResponse.java index 11471ca7d..4283a7bcb 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetScrapFolderResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetScrapFolderResponse.java @@ -1,3 +1,8 @@ package com.shyashyashya.refit.domain.qnaset.dto.response; -public record QnaSetScrapFolderResponse(Long scrapFolderId, String scrapFolderName, boolean contains) {} +import jakarta.validation.constraints.NotNull; + +public record QnaSetScrapFolderResponse( + @NotNull Long scrapFolderId, + @NotNull String scrapFolderName, + @NotNull boolean contains) {} diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetSearchResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetSearchResponse.java index 220508558..1f68afff3 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetSearchResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/dto/response/QnaSetSearchResponse.java @@ -3,11 +3,13 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.qnaset.dto.QnaSetSimpleDto; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record QnaSetSearchResponse(InterviewDto interviewInfo, QnaSetSimpleDto qnaSetInfo) { +public record QnaSetSearchResponse( + @NotNull InterviewDto interviewInfo, @NotNull QnaSetSimpleDto qnaSetInfo) { public static QnaSetSearchResponse from(QnaSet qnaSet) { return QnaSetSearchResponse.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/model/QnaSet.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/model/QnaSet.java index 02f76e1b3..f3519b60c 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/model/QnaSet.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/model/QnaSet.java @@ -79,7 +79,7 @@ public void markDifficult() { } public void unmarkDifficult() { - this.isMarkedDifficult = true; + this.isMarkedDifficult = false; } @Builder(access = AccessLevel.PRIVATE) diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/QnaSetService.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/QnaSetService.java index 8f9ef44f6..2c3ca2916 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/QnaSetService.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/QnaSetService.java @@ -75,6 +75,7 @@ public void unmarkDifficultQuestion(Long qnaSetId) { @Transactional public void updateQnaSet(Long qnaSetId, QnaSetUpdateRequest request) { QnaSet qnaSet = getValidatedQnaSet(qnaSetId); + interviewValidator.validateInterviewReviewStatus(qnaSet.getInterview(), InterviewReviewStatus.QNA_SET_DRAFT); qnaSet.updateQuestionText(request.questionText()); qnaSet.updateAnswerText(request.answerText()); updateOrCreateSelfReview(qnaSet, request.selfReviewText()); @@ -90,6 +91,7 @@ public void deleteQnaSet(Long qnaSetId) { @Transactional public void updatePdfHighlighting(Long qnaSetId, List request) { QnaSet qnaSet = getValidatedQnaSet(qnaSetId); + interviewValidator.validateInterviewReviewStatus(qnaSet.getInterview(), InterviewReviewStatus.QNA_SET_DRAFT); deleteAllHighlightingsAndRects(qnaSet); saveAllHighlightings(qnaSet, request); } @@ -135,13 +137,6 @@ public Page getMyScrapFoldersWithQnaSetContainingInfo return qnaSetScrapFolderRepository.findAllScrapFoldersWithQnaSetContainingInfo(requestUser, qnaSet, pageable); } - private List removeDuplicatedIds(List list) { - if (list == null) { - return null; - } - return list.stream().distinct().toList(); - } - private QnaSet getValidatedQnaSet(Long qnaSetId) { QnaSet qnaSet = qnaSetRepository.findById(qnaSetId).orElseThrow(() -> new CustomException(QNA_SET_NOT_FOUND)); User requestUser = requestUserContext.getRequestUser(); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/StarAnalysisAsyncService.java b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/StarAnalysisAsyncService.java index 62dda0778..77bb971d6 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/StarAnalysisAsyncService.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/qnaset/service/StarAnalysisAsyncService.java @@ -14,6 +14,7 @@ import com.shyashyashya.refit.global.gemini.GeminiClient; import com.shyashyashya.refit.global.gemini.GeminiGenerateRequest; import com.shyashyashya.refit.global.gemini.GeminiGenerateResponse; +import com.shyashyashya.refit.global.gemini.GenerateModel; import com.shyashyashya.refit.global.gemini.StarAnalysisGeminiResponse; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -43,7 +44,10 @@ public CompletableFuture createStarAnalysis(Long qnaSetId) { log.info("Send star analysis generate request to gemini. qnaSetId: {}", qnaSetId); CompletableFuture reqFuture = - geminiClient.sendAsyncRequest(requestBody, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC); + // geminiClient.sendAsyncRequest(requestBody, GenerateModel.GEMINI_2_5_FLASH_LITE, + // STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC); + geminiClient.sendAsyncTextGenerateRequest( + requestBody, GenerateModel.GEMMA_3_27B_IT, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC); return reqFuture .thenApplyAsync( @@ -72,6 +76,12 @@ private StarAnalysisDto processSuccessRequest( private StarAnalysisGeminiResponse parseStarAnalysisGeminiResponse(String text) { try { + if (text.startsWith("```json\n")) { + text = text.substring("```json\n".length()); + } + if (text.endsWith("```")) { + text = text.substring(0, text.length() - "```".length()); + } return objectMapper.readValue(text, StarAnalysisGeminiResponse.class); } catch (JsonProcessingException e) { throw new CustomException(STAR_ANALYSIS_PARSING_FAILED); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/api/ScrapFolderController.java b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/api/ScrapFolderController.java index 2be023e69..44b715a84 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/api/ScrapFolderController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/api/ScrapFolderController.java @@ -14,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; @@ -38,7 +39,8 @@ public class ScrapFolderController { summary = "나의 스크랩 폴더 리스트를 조회합니다.", description = "스크랩 폴더 리스트에 '나의 어려웠던 질문' 폴더는 포함하지 않습니다. 해당 폴더의 내용은 어려웠던 질문을 조회하는 API로 조회합니다.") @GetMapping - public ResponseEntity>> getMyScrapFolders(Pageable pageable) { + public ResponseEntity>> getMyScrapFolders( + @ParameterObject Pageable pageable) { var body = scrapFolderService.getMyScrapFolders(pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); @@ -49,7 +51,7 @@ public ResponseEntity>> getMyScrapFolders( description = "'나의 어려웠던 질문' 폴더는 포함하지 않습니다. 해당 폴더의 내용은 어려웠던 질문을 조회하는 API로 조회합니다.") @GetMapping("/{scrapFolderId}") public ResponseEntity>> getQnaSetsInScrapFolder( - @PathVariable Long scrapFolderId, Pageable pageable) { + @PathVariable Long scrapFolderId, @ParameterObject Pageable pageable) { var body = scrapFolderService.getQnaSetsInScrapFolder(scrapFolderId, pageable); var response = ApiResponse.success(COMMON200, body); return ResponseEntity.ok(response); diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderQnaSetResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderQnaSetResponse.java index 7a02f3290..604fc7cf7 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderQnaSetResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderQnaSetResponse.java @@ -3,11 +3,13 @@ import com.shyashyashya.refit.domain.interview.dto.InterviewDto; import com.shyashyashya.refit.domain.qnaset.dto.QnaSetSimpleDto; import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record ScrapFolderQnaSetResponse(InterviewDto interview, QnaSetSimpleDto qnaSet) { +public record ScrapFolderQnaSetResponse( + @NotNull InterviewDto interview, @NotNull QnaSetSimpleDto qnaSet) { public static ScrapFolderQnaSetResponse from(QnaSet qnaSet) { return ScrapFolderQnaSetResponse.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderResponse.java index 3054421e6..7acb64606 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/scrapfolder/dto/response/ScrapFolderResponse.java @@ -1,11 +1,15 @@ package com.shyashyashya.refit.domain.scrapfolder.dto.response; import com.shyashyashya.refit.domain.scrapfolder.model.ScrapFolder; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record ScrapFolderResponse(Long scrapFolderId, String scrapFolderName, Long qnaSetCount) { +public record ScrapFolderResponse( + @NotNull Long scrapFolderId, + @NotNull String scrapFolderName, + @NotNull Long qnaSetCount) { public static ScrapFolderResponse from(ScrapFolder scrapFolder, Long qnaSetCount) { return ScrapFolderResponse.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/user/api/TestUserController.java b/backend/src/main/java/com/shyashyashya/refit/domain/user/api/TestUserController.java index 379165532..75f27c301 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/user/api/TestUserController.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/user/api/TestUserController.java @@ -1,32 +1,47 @@ package com.shyashyashya.refit.domain.user.api; +import static com.shyashyashya.refit.domain.qnaset.constant.StarAnalysisConstant.STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC; +import static com.shyashyashya.refit.global.exception.ErrorCode.TEXT_EMBEDDING_CREATE_FAILED; import static com.shyashyashya.refit.global.exception.ErrorCode.USER_NOT_FOUND; +import static com.shyashyashya.refit.global.model.ResponseCode.COMMON200; import static com.shyashyashya.refit.global.model.ResponseCode.COMMON204; import com.shyashyashya.refit.domain.user.repository.UserRepository; import com.shyashyashya.refit.global.auth.repository.RefreshTokenRepository; import com.shyashyashya.refit.global.dto.ApiResponse; import com.shyashyashya.refit.global.exception.CustomException; +import com.shyashyashya.refit.global.gemini.GeminiClient; +import com.shyashyashya.refit.global.gemini.GeminiEmbeddingRequest; +import com.shyashyashya.refit.global.gemini.GeminiEmbeddingResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Test Auth/User API", description = "개발용 테스트 인증/인가 API입니다.") +@Tag(name = "Test API", description = "개발용 테스트 API입니다.") @RestController @RequestMapping("/test/user") @RequiredArgsConstructor +@Slf4j public class TestUserController { private final UserRepository userRepository; private final RefreshTokenRepository refreshTokenRepository; + private final GeminiClient geminiClient; + private final Executor geminiPostProcessExecutor; @Operation(summary = "(테스트용) 유저를 이메일로 찾아 삭제합니다.") @DeleteMapping @@ -61,4 +76,26 @@ public ResponseEntity> deleteUserById(@PathVariable Long userI var body = ApiResponse.success(COMMON204); return ResponseEntity.ok(body); } + + // TODO API 삭제: Gemini Embedding 생성 테스트용 임시 메소드 + @Operation(summary = "(테스트) 요청 텍스트의 임베딩값을 생성합니다.") + @PostMapping("/test-embedding") + public CompletableFuture>> getGeminiEmbedding( + @RequestBody @NotBlank String text) { + + GeminiEmbeddingRequest requestBody = GeminiEmbeddingRequest.of( + text, GeminiEmbeddingRequest.TaskType.CLUSTERING, GeminiEmbeddingRequest.OutputDimensionality.D128); + + CompletableFuture reqFuture = + geminiClient.sendAsyncEmbeddingRequest(requestBody, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC); + + CompletableFuture result = reqFuture + .thenApplyAsync(response -> response, geminiPostProcessExecutor) + .exceptionally(e -> { + log.error(e.getMessage(), e); + throw new CustomException(TEXT_EMBEDDING_CREATE_FAILED); + }); + + return result.thenApply(rsp -> ResponseEntity.ok(ApiResponse.success(COMMON200, rsp))); + } } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/user/dto/response/MyProfileResponse.java b/backend/src/main/java/com/shyashyashya/refit/domain/user/dto/response/MyProfileResponse.java index cafc346f1..7592ca663 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/user/dto/response/MyProfileResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/user/dto/response/MyProfileResponse.java @@ -1,11 +1,17 @@ package com.shyashyashya.refit.domain.user.dto.response; import com.shyashyashya.refit.domain.user.model.User; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record MyProfileResponse(String nickname, Long industryId, Long jobCategoryId, String profileImageUrl) { +public record MyProfileResponse( + @NotNull String nickname, + @NotNull Long industryId, + @NotNull Long jobCategoryId, + @NotNull String profileImageUrl, + @NotNull boolean isAgreedToTerms) { public static MyProfileResponse from(User user) { return MyProfileResponse.builder() @@ -13,6 +19,7 @@ public static MyProfileResponse from(User user) { .industryId(user.getIndustry().getId()) .jobCategoryId(user.getJobCategory().getId()) .profileImageUrl(user.getProfileImageUrl()) + .isAgreedToTerms(user.isAgreedToTerms()) .build(); } } diff --git a/backend/src/main/java/com/shyashyashya/refit/domain/user/model/User.java b/backend/src/main/java/com/shyashyashya/refit/domain/user/model/User.java index 50e6904f7..36dfa5905 100644 --- a/backend/src/main/java/com/shyashyashya/refit/domain/user/model/User.java +++ b/backend/src/main/java/com/shyashyashya/refit/domain/user/model/User.java @@ -34,7 +34,7 @@ public class User extends BaseEntity { @Column(columnDefinition = "varchar(30)", nullable = false, unique = true) private String nickname; - @Column(columnDefinition = "varchar(2048)") + @Column(columnDefinition = "varchar(2048)", nullable = false) private String profileImageUrl; @Column(nullable = false) diff --git a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenPairDto.java b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenPairDto.java index cfeb13057..4d05025b0 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenPairDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenPairDto.java @@ -1,6 +1,9 @@ package com.shyashyashya.refit.global.auth.dto; -public record TokenPairDto(String accessToken, String refreshToken) { +import jakarta.validation.constraints.NotNull; + +public record TokenPairDto( + @NotNull String accessToken, @NotNull String refreshToken) { public static TokenPairDto of(String accessToken, String refreshToken) { return new TokenPairDto(accessToken, refreshToken); diff --git a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenReissueResultDto.java b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenReissueResultDto.java index a6411995d..108b42f71 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenReissueResultDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/TokenReissueResultDto.java @@ -1,11 +1,15 @@ package com.shyashyashya.refit.global.auth.dto; import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record TokenReissueResultDto(boolean isReissueProcessed, boolean isNeedSignUp, TokenPairDto tokenPair) { +public record TokenReissueResultDto( + @NotNull boolean isReissueProcessed, + @NotNull boolean isNeedSignUp, + @NotNull TokenPairDto tokenPair) { public static TokenReissueResultDto createReissueProcessed( @Nullable Long userId, String accessToken, String refreshToken) { diff --git a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TestPublishTokenResponse.java b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TestPublishTokenResponse.java index 7c6305953..0b6a07dad 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TestPublishTokenResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TestPublishTokenResponse.java @@ -1,11 +1,13 @@ package com.shyashyashya.refit.global.auth.dto.response; import com.shyashyashya.refit.global.auth.dto.TokenPairDto; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record TestPublishTokenResponse(boolean isNeedSignUp, TokenPairDto tokens) { +public record TestPublishTokenResponse( + @NotNull boolean isNeedSignUp, @NotNull TokenPairDto tokens) { public static TestPublishTokenResponse of(boolean isNeedSignUp, TokenPairDto tokenPair) { return TestPublishTokenResponse.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TokenReissueResponse.java b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TokenReissueResponse.java index 398e401c3..af8c3fd63 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TokenReissueResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/auth/dto/response/TokenReissueResponse.java @@ -1,10 +1,12 @@ package com.shyashyashya.refit.global.auth.dto.response; import com.shyashyashya.refit.global.auth.dto.TokenReissueResultDto; +import jakarta.validation.constraints.NotNull; import lombok.Builder; @Builder(access = lombok.AccessLevel.PRIVATE) -public record TokenReissueResponse(boolean isReissueProcessed, boolean isNeedSignUp) { +public record TokenReissueResponse( + @NotNull boolean isReissueProcessed, @NotNull boolean isNeedSignUp) { public static TokenReissueResponse from(TokenReissueResultDto tokenReissueResultDto) { return TokenReissueResponse.builder() diff --git a/backend/src/main/java/com/shyashyashya/refit/global/dto/ApiResponse.java b/backend/src/main/java/com/shyashyashya/refit/global/dto/ApiResponse.java index c5539abe6..bebb77f82 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/dto/ApiResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/dto/ApiResponse.java @@ -2,8 +2,13 @@ import com.shyashyashya.refit.global.exception.ErrorCode; import com.shyashyashya.refit.global.model.ResponseCode; +import jakarta.validation.constraints.NotNull; -public record ApiResponse(boolean isSuccess, String code, String message, T result) { +public record ApiResponse( + @NotNull boolean isSuccess, + @NotNull String code, + @NotNull String message, + T result) { public static ApiResponse success(ResponseCode responseCode) { return new ApiResponse<>(true, responseCode.name(), responseCode.getMessage(), null); diff --git a/backend/src/main/java/com/shyashyashya/refit/global/exception/ErrorCode.java b/backend/src/main/java/com/shyashyashya/refit/global/exception/ErrorCode.java index 2eedc3525..34201c182 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/exception/ErrorCode.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/exception/ErrorCode.java @@ -17,9 +17,8 @@ public enum ErrorCode { INTERVIEW_NOT_ACCESSIBLE(FORBIDDEN, "인터뷰에 접근할 수 없습니다."), INTERVIEW_NOT_IN_DRAFT_STATUS(BAD_REQUEST, "임시저장 상태의 인터뷰만 조회할 수 있습니다."), INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_LOG_DRAFT(BAD_REQUEST, "인터뷰 상태가 기록전 상태일 때만 기록중으로 바뀔 수 있습니다"), - INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_QNA_SET_DRAFT(BAD_REQUEST, "인터뷰 상태가 기록중 상태일 때만 QnaSetDraft로 바뀔 수 있습니다"), - INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_SELF_REVIEW_DRAFT( - BAD_REQUEST, "인터뷰 상태가 QnaSetDraft 상태일 때만 회고중으로 바뀔 수 있습니다"), + INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_QNA_SET_DRAFT(BAD_REQUEST, "인터뷰 상태가 기록중 상태일 때만 질답세트검토중으로 바뀔 수 있습니다"), + INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_SELF_REVIEW_DRAFT(BAD_REQUEST, "인터뷰 상태가 질답세트검토중 상태일 때만 회고중으로 바뀔 수 있습니다"), INTERVIEW_REVIEW_STATUS_NOT_UPDATABLE_TO_DEBRIEF_COMPLETED(BAD_REQUEST, "인터뷰 상태가 회고중 상태일 때만 회고완료로 바뀔 수 있습니다"), INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED(BAD_REQUEST, "요청을 처리하는데 필요한 인터뷰 상태의 검증에 실패하였습니다."), @@ -57,7 +56,8 @@ public enum ErrorCode { STAR_ANALYSIS_CREATE_FAILED(INTERNAL_SERVER_ERROR, "스타 분석 생성 중 오류가 발생하였습니다."), STAR_ANALYSIS_COMPLETE_FAILED(INTERNAL_SERVER_ERROR, "스타 분석 업데이트 중 오류가 발생하였습니다."), STAR_ANALYSIS_DELETE_NOT_ALLOWED_STATUS(BAD_REQUEST, "진행 중(IN_PROGRESS)인 스타 분석만 삭제할 수 있습니다."), - ; + + TEXT_EMBEDDING_CREATE_FAILED(INTERNAL_SERVER_ERROR, "임베딩 생성에 실패하였습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiClient.java b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiClient.java index 82d2b9b81..8e2d057f1 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiClient.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiClient.java @@ -15,17 +15,11 @@ public class GeminiClient { private final GeminiProperty geminiProperty; private final WebClient webClient; - // TODO 요청 URL 상수로 분리, 임베딩 요청 고려 - private static final String GEMINI_API_URL = - "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"; - // "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"; - // "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent"; - - public CompletableFuture sendAsyncRequest( - GeminiGenerateRequest requestBody, Long timeoutSec) { + public CompletableFuture sendAsyncTextGenerateRequest( + GeminiGenerateRequest requestBody, GenerateModel model, Long timeoutSec) { return webClient .post() - .uri(GEMINI_API_URL) + .uri(model.endpoint()) .header("x-goog-api-key", geminiProperty.apiKey()) .accept(MediaType.APPLICATION_JSON) .bodyValue(requestBody) @@ -34,4 +28,21 @@ public CompletableFuture sendAsyncRequest( .timeout(Duration.ofSeconds(timeoutSec)) .toFuture(); } + + private static final String EMBEDDING_ENDPOINT = + "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent"; + + public CompletableFuture sendAsyncEmbeddingRequest( + GeminiEmbeddingRequest requestBody, Long timeoutSec) { + return webClient + .post() + .uri(EMBEDDING_ENDPOINT) + .header("x-goog-api-key", geminiProperty.apiKey()) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(GeminiEmbeddingResponse.class) + .timeout(Duration.ofSeconds(timeoutSec)) + .toFuture(); + } } diff --git a/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingRequest.java b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingRequest.java new file mode 100644 index 000000000..07769fc84 --- /dev/null +++ b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingRequest.java @@ -0,0 +1,46 @@ +package com.shyashyashya.refit.global.gemini; + +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.List; + +public record GeminiEmbeddingRequest(TaskType taskType, Content content, OutputDimensionality outputDimensionality) { + + public enum TaskType { + SEMANTIC_SIMILARITY, + CLASSIFICATION, + CLUSTERING + } + + public enum OutputDimensionality { + D2048(2048), + D1536(1536), + D768(768), + D512(512), + D256(256), + D128(128); + + private final int value; + + OutputDimensionality(int value) { + this.value = value; + } + + @JsonValue + public int value() { + return value; + } + } + + public record Content(List parts) {} + + public record Part(String text) {} + + public static GeminiEmbeddingRequest of(String text, TaskType taskType, OutputDimensionality outputDimensionality) { + if (text == null || text.isBlank()) { + throw new IllegalArgumentException("text must not be blank"); + } + + Content content = new Content(List.of(new Part(text))); + return new GeminiEmbeddingRequest(taskType, content, outputDimensionality); + } +} diff --git a/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingResponse.java b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingResponse.java new file mode 100644 index 000000000..3bf4ca87a --- /dev/null +++ b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GeminiEmbeddingResponse.java @@ -0,0 +1,8 @@ +package com.shyashyashya.refit.global.gemini; + +import java.util.List; + +public record GeminiEmbeddingResponse(Embedding embedding) { + + public record Embedding(List values) {} +} diff --git a/backend/src/main/java/com/shyashyashya/refit/global/gemini/GenerateModel.java b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GenerateModel.java new file mode 100644 index 000000000..5170e8be9 --- /dev/null +++ b/backend/src/main/java/com/shyashyashya/refit/global/gemini/GenerateModel.java @@ -0,0 +1,33 @@ +package com.shyashyashya.refit.global.gemini; + +public enum GenerateModel { + GEMINI_2_5_PRO("gemini-2.5-pro"), + GEMINI_2_5_FLASH_LITE("gemini-2.5-flash-lite"), + GEMINI_2_5_FLASH("gemini-2.5-flash"), + GEMINI_3_FLASH("gemini-3-flash-preview"), + GEMINI_3_PRO("gemini-3-pro-preview"), + + GEMMA_3_1B_IT("gemma-3-1b-it"), + GEMMA_3_4B_IT("gemma-3-4b-it"), + GEMMA_3_12B_IT("gemma-3-12b-it"), + GEMMA_3_27B_IT("gemma-3-27b-it"); + + private static final String PREFIX = "https://generativelanguage.googleapis.com/v1beta/models/"; + private static final String SUFFIX = ":generateContent"; + + private final String name; + private final String endpoint; + + GenerateModel(String name) { + this.name = name; + this.endpoint = PREFIX + name + SUFFIX; + } + + public String id() { + return name; + } + + public String endpoint() { + return endpoint; + } +} diff --git a/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/OAuth2ResultDto.java b/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/OAuth2ResultDto.java index ddb70d35e..3ad736893 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/OAuth2ResultDto.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/OAuth2ResultDto.java @@ -2,14 +2,15 @@ import com.shyashyashya.refit.global.auth.dto.TokenPairDto; import com.shyashyashya.refit.global.model.ClientOriginType; +import jakarta.validation.constraints.NotNull; import lombok.Builder; @Builder public record OAuth2ResultDto( - TokenPairDto tokenPair, - ClientOriginType clientOriginType, - boolean isNeedSignup, - String nickname, + @NotNull TokenPairDto tokenPair, + @NotNull ClientOriginType clientOriginType, + @NotNull boolean isNeedSignup, + @NotNull String nickname, String profileImageUrl) { public static OAuth2ResultDto of( diff --git a/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/response/OAuth2LoginUrlResponse.java b/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/response/OAuth2LoginUrlResponse.java index e064f28eb..78beadaf5 100644 --- a/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/response/OAuth2LoginUrlResponse.java +++ b/backend/src/main/java/com/shyashyashya/refit/global/oauth2/dto/response/OAuth2LoginUrlResponse.java @@ -1,10 +1,11 @@ package com.shyashyashya.refit.global.oauth2.dto.response; +import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Builder; @Builder(access = AccessLevel.PRIVATE) -public record OAuth2LoginUrlResponse(String oAuth2LoginUrl) { +public record OAuth2LoginUrlResponse(@NotNull String oAuth2LoginUrl) { public static OAuth2LoginUrlResponse from(String oAuth2LoginUrl) { return OAuth2LoginUrlResponse.builder().oAuth2LoginUrl(oAuth2LoginUrl).build(); diff --git a/backend/src/test/java/com/shyashyashya/refit/core/IntegrationTest.java b/backend/src/test/java/com/shyashyashya/refit/core/IntegrationTest.java index 49769b325..3d49c188b 100644 --- a/backend/src/test/java/com/shyashyashya/refit/core/IntegrationTest.java +++ b/backend/src/test/java/com/shyashyashya/refit/core/IntegrationTest.java @@ -7,11 +7,28 @@ import com.shyashyashya.refit.domain.industry.model.Industry; import com.shyashyashya.refit.domain.industry.repository.IndustryRepository; import com.shyashyashya.refit.domain.interview.dto.request.InterviewCreateRequest; +import com.shyashyashya.refit.domain.interview.dto.request.QnaSetCreateRequest; import com.shyashyashya.refit.domain.interview.model.Interview; import com.shyashyashya.refit.domain.interview.model.InterviewReviewStatus; import com.shyashyashya.refit.domain.interview.repository.InterviewRepository; import com.shyashyashya.refit.domain.jobcategory.model.JobCategory; import com.shyashyashya.refit.domain.jobcategory.repository.JobCategoryRepository; +import com.shyashyashya.refit.domain.qnaset.dto.request.PdfHighlightingUpdateRequest; +import com.shyashyashya.refit.domain.qnaset.model.PdfHighlighting; +import com.shyashyashya.refit.domain.qnaset.model.PdfHighlightingRect; +import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import com.shyashyashya.refit.domain.qnaset.model.QnaSetCategory; +import com.shyashyashya.refit.domain.qnaset.repository.PdfHighlightingRectRepository; +import com.shyashyashya.refit.domain.qnaset.repository.PdfHighlightingRepository; +import com.shyashyashya.refit.domain.qnaset.repository.QnaSetCategoryRepository; +import com.shyashyashya.refit.domain.qnaset.repository.QnaSetRepository; +import com.shyashyashya.refit.domain.qnaset.dto.request.PdfHighlightingUpdateRequest; +import com.shyashyashya.refit.domain.qnaset.model.PdfHighlighting; +import com.shyashyashya.refit.domain.qnaset.model.PdfHighlightingRect; +import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import com.shyashyashya.refit.domain.qnaset.repository.PdfHighlightingRectRepository; +import com.shyashyashya.refit.domain.qnaset.repository.PdfHighlightingRepository; +import com.shyashyashya.refit.domain.qnaset.repository.QnaSetRepository; import com.shyashyashya.refit.domain.user.model.User; import com.shyashyashya.refit.domain.user.repository.UserRepository; import com.shyashyashya.refit.global.auth.service.JwtEncoder; @@ -58,6 +75,9 @@ public abstract class IntegrationTest { protected Company company1; protected Company company2; protected Company company3; + protected QnaSetCategory qnaSetCategory1; + protected QnaSetCategory qnaSetCategory2; + protected QnaSetCategory qnaSetCategory3; @PersistenceContext private EntityManager em; @@ -80,6 +100,18 @@ public abstract class IntegrationTest { @Autowired private CompanyRepository companyRepository; + @Autowired + private QnaSetRepository qnaSetRepository; + + @Autowired + private PdfHighlightingRepository pdfHighlightingRepository; + + @Autowired + private PdfHighlightingRectRepository pdfHighlightingRectRepository; + + @Autowired + private QnaSetCategoryRepository qnaSetCategoryRepository; + @BeforeEach void restAssuredSetUp() { clearDatabase(); @@ -94,6 +126,9 @@ void restAssuredSetUp() { company1 = companyRepository.save(Company.create("현대자동차", "logo1", true)); company2 = companyRepository.save(Company.create("카카오", "logo2", true)); company3 = companyRepository.save(Company.create("네이버", "logo3", true)); + qnaSetCategory1 = qnaSetCategoryRepository.save(QnaSetCategory.create("리더십 질문", "당신은 리더십있는 사람입니까?", 3.141592)); + qnaSetCategory2 = qnaSetCategoryRepository.save(QnaSetCategory.create("인성 질문", "당신은 인성이 좋은 사람입니까?", 2.145)); + qnaSetCategory3 = qnaSetCategoryRepository.save(QnaSetCategory.create("기술 질문", "당신은 기술 있는 사람입니까?", 0.001)); requestUser = createAndSaveUser("test@example.com", "default", industry1, jobCategory1); Instant issuedAt = Instant.now(); @@ -188,4 +223,57 @@ protected Company createAndSaveCompany(String companyName) { Company company = Company.create(companyName, "logo.url", true); return companyRepository.save(company); } + + protected QnaSet createAndSaveQnaSet(QnaSetCreateRequest request, Interview interview) { + return createAndSaveQnaSet(request, interview, false); + } + + protected QnaSet createAndSaveQnaSet(QnaSetCreateRequest request, Interview interview, boolean isMarkedDifficult) { + QnaSet qnaSet = QnaSet.create( + request.questionText(), + request.answerText(), + isMarkedDifficult, + interview, + null + ); + + return qnaSetRepository.save(qnaSet); + } + + protected QnaSet createAndSaveQnaSet(QnaSetCreateRequest request, Interview interview, QnaSetCategory qnaSetCategory) { + QnaSet qnaSet = QnaSet.create( + request.questionText(), + request.answerText(), + false, + interview, + qnaSetCategory + ); + + return qnaSetRepository.save(qnaSet); + } + + protected List createAndSavePdfHighlighting(List requests, QnaSet qnaSet) { + List result = new ArrayList<>(); + + requests.forEach(request -> { + PdfHighlighting pdfHighlighting = PdfHighlighting.create(request.highlightingText(), qnaSet); + result.add(pdfHighlightingRepository.save(pdfHighlighting)); + + request.rects().forEach( + rectDto -> { + PdfHighlightingRect rect = PdfHighlightingRect.create( + rectDto.x(), + rectDto.y(), + rectDto.width(), + rectDto.height(), + rectDto.pageNumber(), + pdfHighlighting); + + pdfHighlightingRectRepository.save(rect); + } + ); + }); + + return result; + } } diff --git a/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetIntegrationTest.java b/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetIntegrationTest.java new file mode 100644 index 000000000..348ab0bbf --- /dev/null +++ b/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetIntegrationTest.java @@ -0,0 +1,578 @@ +package com.shyashyashya.refit.integration.qnaset; + +import static com.shyashyashya.refit.global.exception.ErrorCode.INTERVIEW_NOT_ACCESSIBLE; +import static com.shyashyashya.refit.global.exception.ErrorCode.INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED; +import static com.shyashyashya.refit.global.exception.ErrorCode.QNA_SET_NOT_FOUND; +import static com.shyashyashya.refit.global.model.ResponseCode.COMMON200; +import static com.shyashyashya.refit.global.model.ResponseCode.COMMON204; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import com.shyashyashya.refit.core.IntegrationTest; +import com.shyashyashya.refit.domain.interview.dto.request.InterviewCreateRequest; +import com.shyashyashya.refit.domain.interview.dto.request.QnaSetCreateRequest; +import com.shyashyashya.refit.domain.interview.model.Interview; +import com.shyashyashya.refit.domain.interview.model.InterviewReviewStatus; +import com.shyashyashya.refit.domain.interview.model.InterviewType; +import com.shyashyashya.refit.domain.interview.repository.InterviewRepository; +import com.shyashyashya.refit.domain.qnaset.dto.PdfHighlightingRectDto; +import com.shyashyashya.refit.domain.qnaset.dto.request.PdfHighlightingUpdateRequest; +import com.shyashyashya.refit.domain.qnaset.dto.request.QnaSetUpdateRequest; +import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import com.shyashyashya.refit.domain.qnaset.repository.QnaSetRepository; +import com.shyashyashya.refit.domain.user.model.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +public class QnaSetIntegrationTest extends IntegrationTest { + + @Autowired + private InterviewRepository interviewRepository; + @Autowired + private QnaSetRepository qnaSetRepository; + + private Long qnaSetDraftQnaSetId; + private Long debriefCompletedQnaSetId; + private Long qnaSetWithPdfHighlightingId; + private Long otherUserQnaSetId; + + @BeforeEach + void setUp() { + var interviewCreateRequest1 = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + Interview qnaSetDraftInterview = createAndSaveInterview(interviewCreateRequest1, InterviewReviewStatus.QNA_SET_DRAFT); + + var interviewCreateRequest2 = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + Interview debriefCompletedInterview = createAndSaveInterview(interviewCreateRequest2, InterviewReviewStatus.DEBRIEF_COMPLETED); + + var qnaSetCreateRequest1 = new QnaSetCreateRequest("test question text", "test answer text"); + QnaSet qnaSetDraftQnaSet = createAndSaveQnaSet(qnaSetCreateRequest1, qnaSetDraftInterview, true); + qnaSetDraftQnaSetId = qnaSetDraftQnaSet.getId(); + + var qnaSetCreateRequest2 = new QnaSetCreateRequest("test question text", "test answer text"); + QnaSet debriefCompletedQnaSet = createAndSaveQnaSet(qnaSetCreateRequest2, debriefCompletedInterview, true); + debriefCompletedQnaSetId = debriefCompletedQnaSet.getId(); + + var qnaSetCreateRequest3 = new QnaSetCreateRequest("this qna has pdf highlighting", "hello PDF"); + QnaSet qnaSetWithPdfHighlighting = createAndSaveQnaSet(qnaSetCreateRequest3,qnaSetDraftInterview, false); + qnaSetWithPdfHighlightingId = qnaSetWithPdfHighlighting.getId(); + + var interviewCreateRequest4 = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + User user = createAndSaveUser("other@example.com", "other", industry1, jobCategory1); + Interview otherUserInterview = createAndSaveInterview(interviewCreateRequest4, InterviewReviewStatus.NOT_LOGGED, user); + + QnaSetCreateRequest qnaSetCreateRequest4 = new QnaSetCreateRequest("this qna is others", "hello stranger"); + QnaSet otherUserQnaSet = createAndSaveQnaSet(qnaSetCreateRequest4, otherUserInterview, false); + otherUserQnaSetId = otherUserQnaSet.getId(); + + List pdfHighlightUpdateRequest = createPdfHighlightUpdateRequest(); + createAndSavePdfHighlighting(pdfHighlightUpdateRequest, qnaSetWithPdfHighlighting); + } + + @Nested + class 질답_세트_생성_시 { + + @Test + void 인터뷰가_질답_세트_검토_중_상태이면_질답_세트_생성에_성공한다() { + // given + InterviewCreateRequest interviewCreateRequest = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + Interview interview1 = createAndSaveInterview(interviewCreateRequest, InterviewReviewStatus.QNA_SET_DRAFT); + QnaSetCreateRequest request = new QnaSetCreateRequest("test question text", "test answer text"); + + // when & then + given(spec) + .body(request) + .when() + .post("/interview/" + interview1.getId() + "/qna-set") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()) + .body("result.qnaSetId", notNullValue()); + } + + @ParameterizedTest + @EnumSource( + value = InterviewReviewStatus.class, + mode = EnumSource.Mode.EXCLUDE, + names = "QNA_SET_DRAFT" + ) + void 인터뷰가_질답_세트_검토_중_상태가_아니라면_질답_세트_생성에_실패한다(InterviewReviewStatus reviewStatus) { + // given + InterviewCreateRequest interviewCreateRequest = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + + Interview interview = createAndSaveInterview(interviewCreateRequest, reviewStatus); + QnaSetCreateRequest request = new QnaSetCreateRequest("test question text", "test answer text"); + + // when & then + given(spec) + .body(request) + .when() + .post("/interview/" + interview.getId() + "/qna-set") + .then() + .statusCode(400) + .body("code", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.name())) + .body("message", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.getMessage())) + .body("result", nullValue()); + } + } + + @Nested + class 어려웠던_질문_마킹_시 { + + private Interview interview; + private Long qnaSetId; + + @BeforeEach + void setUp() { + interview = createAndSaveInterview( + new InterviewCreateRequest( + LocalDateTime.of(2023, 1, 10, 10, 0, 0), InterviewType.FIRST, company1.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + )); + QnaSetCreateRequest qnaSetCreateRequest = new QnaSetCreateRequest ("test qqq text", "test aaa text"); + QnaSet qnaSet = createAndSaveQnaSet(qnaSetCreateRequest, interview, false); + qnaSetId = qnaSet.getId(); + } + + @Test + void 어려웠던_질문_마킹에_성공한다() { + // given + + // when & then + given(spec). + when(). + patch("/qna-set/" + qnaSetId + "/difficult/mark"). + then(). + statusCode(200). + body("code", equalTo(COMMON200.name())). + body("message", equalTo(COMMON200.getMessage())). + body("result", nullValue()); + + QnaSet result = qnaSetRepository.findById(qnaSetId).get(); + assertThat(result.isMarkedDifficult()).isTrue(); + } + + @Test + void 어려웠던_질문_마킹_해제에_성공한다() { + // given + + // when & then + given(spec). + when(). + patch("/qna-set/" + qnaSetId + "/difficult/unmark"). + then(). + statusCode(200). + body("code", equalTo(COMMON200.name())). + body("message", equalTo(COMMON200.getMessage())). + body("result", nullValue()); + + QnaSet result = qnaSetRepository.findById(qnaSetId).get(); + assertThat(result.isMarkedDifficult()).isFalse(); + } + } + + @Nested + class 질답_세트_수정_시 { + + @Test + void 인터뷰가_질답_세트_검토_중_상태이면_질답_세트_수정에_성공한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("update question", "update answer", "self review self review"); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + + QnaSet updated = qnaSetRepository.findById(qnaSetDraftQnaSetId).get(); + assertThat(updated.getQuestionText()).isEqualTo("update question"); + assertThat(updated.getAnswerText()).isEqualTo("update answer"); + } + + @Test + void 수정_요청에_질문만_존재할_때_질답_세트_수정에_성공한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("only question text update", null, null); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + + QnaSet updated = qnaSetRepository.findById(qnaSetDraftQnaSetId).get(); + assertThat(updated.getQuestionText()).isEqualTo("only question text update"); + assertThat(updated.getAnswerText()).isEqualTo("test answer text"); + } + + @Test + void 수정_요청에_답변만_존재할_때_질답_세트_수정에_성공한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest(null, "only answer text update", null); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + + QnaSet updated = qnaSetRepository.findById(qnaSetDraftQnaSetId).get(); + assertThat(updated.getQuestionText()).isEqualTo("test question text"); + assertThat(updated.getAnswerText()).isEqualTo("only answer text update"); + } + + @Test + void 수정_요청이_빈_문자열일_때_질답_세트_수정에_성공한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("", null, ""); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + } + + @Test + void 인터뷰가_질답_세트_검토_중_상태가_아니라면_질답_세트_수정에_실패한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("update question", "update answer", null); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + debriefCompletedQnaSetId) + .then() + .statusCode(400) + .body("code", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.name())) + .body("message", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.getMessage())) + .body("result", nullValue()); + } + + @Test + void 질답_세트가_존재하지_않으면_질답_세트_수정에_실패한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("update question", "update answer", null); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + Long.MAX_VALUE) + .then() + .statusCode(404) + .body("code", equalTo(QNA_SET_NOT_FOUND.name())) + .body("message", equalTo(QNA_SET_NOT_FOUND.getMessage())) + .body("result", nullValue()); + } + + @Test + void 다른_사람의_질답_세트_수정에_실패한다() { + // given + QnaSetUpdateRequest qnaSetUpdateRequest = new QnaSetUpdateRequest("update question", "update answer", null); + + // when & then + given(spec) + .body(qnaSetUpdateRequest) + .when() + .put("/qna-set/" + otherUserQnaSetId) + .then() + .statusCode(403) + .body("code", equalTo(INTERVIEW_NOT_ACCESSIBLE.name())) + .body("message", equalTo(INTERVIEW_NOT_ACCESSIBLE.getMessage())) + .body("result", nullValue()); + } + } + + @Nested + class 질답_세트_삭제_시 { + + @Test + void 인터뷰가_질답_세트_검토_중_상태이면_질답_세트_삭제에_성공한다() { + // given + + // when & then + given(spec) + .when() + .delete("/qna-set/" + qnaSetDraftQnaSetId) + .then() + .statusCode(200) + .body("code", equalTo(COMMON204.name())) + .body("message", equalTo(COMMON204.getMessage())) + .body("result", nullValue()); + } + + @Test + void 인터뷰가_질답_세트_검토_중_상태가_아니면_질답_세트_삭제에_실패한다() { + // given + + // when & then + given(spec) + .when() + .delete("/qna-set/" + debriefCompletedQnaSetId) + .then() + .statusCode(400) + .body("code", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.name())) + .body("message", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.getMessage())) + .body("result", nullValue()); + } + @Test + void 질답_세트가_존재하지_않으면_질답_세트_삭제에_실패한다() { + // given + + // when & then + given(spec) + .when() + .delete("/qna-set/" + Long.MAX_VALUE) + .then() + .statusCode(404) + .body("code", equalTo(QNA_SET_NOT_FOUND.name())) + .body("message", equalTo(QNA_SET_NOT_FOUND.getMessage())) + .body("result", nullValue()); + } + + @Test + void 다른_사람의_질답_세트_삭제에_실패한다() { + // given + + // when & then + given(spec) + .when() + .delete("/qna-set/" + otherUserQnaSetId) + .then() + .statusCode(403) + .body("code", equalTo(INTERVIEW_NOT_ACCESSIBLE.name())) + .body("message", equalTo(INTERVIEW_NOT_ACCESSIBLE.getMessage())) + .body("result", nullValue()); + } + } + + @Nested + class PDF_하이라이팅_등록_수정_시 { + + @Test + void 인터뷰가_질답_세트_검토_중_상태이면_PDF_하이라이팅_등록에_성공한다() { + // given + List request = createPdfHighlightUpdateRequest(); + + // when & then + given(spec) + .body(request) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId + "/pdf-highlightings") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + } + + @Test + void 인터뷰가_질답_세트_검토_중_상태이면_비어있는_PDF_하이라이팅_등록에_성공한다() { + // given + List request = List.of(); + + // when & then + given(spec) + .body(request) + .when() + .put("/qna-set/" + qnaSetDraftQnaSetId + "/pdf-highlightings") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", nullValue()); + } + + @ParameterizedTest + @EnumSource( + value = InterviewReviewStatus.class, + mode = EnumSource.Mode.EXCLUDE, + names = "QNA_SET_DRAFT" + ) + void 인터뷰가_질답_세트_검토_중_상태가_아니면_PDF_하이라이팅_등록에_실패한다(InterviewReviewStatus reviewStatus) { + // given + var interviewCreateRequest = new InterviewCreateRequest( + LocalDateTime.of(2025, 12, 29, 10, 0, 0), InterviewType.FIRST, "현대자동차", 1L, 1L, "BE Developer"); + var interview = createAndSaveInterview(interviewCreateRequest, reviewStatus); + var qnaSetCreateRequest = new QnaSetCreateRequest("test question text", "test answer text"); + var qnaSetId = createAndSaveQnaSet(qnaSetCreateRequest, interview).getId(); + + List request = createPdfHighlightUpdateRequest(); + + // when & then + given(spec) + .body(request) + .when() + .put("/qna-set/" + qnaSetId + "/pdf-highlightings") + .then() + .statusCode(400) + .body("code", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.name())) + .body("message", equalTo(INTERVIEW_REVIEW_STATUS_VALIDATION_FAILED.getMessage())) + .body("result", nullValue()); + } + + @Test + void 존재하지_않는_질답에_PDF_하이라이팅_등록을_시도하면_실패한다() { + // given + List request = createPdfHighlightUpdateRequest(); + + // when & then + given(spec) + .body(request) + .when() + .put("/qna-set/" + Long.MAX_VALUE + "/pdf-highlightings") + .then() + .statusCode(404) + .body("code", equalTo(QNA_SET_NOT_FOUND.name())) + .body("message", equalTo(QNA_SET_NOT_FOUND.getMessage())) + .body("result", nullValue()); + } + + @Test + void 타인의_질답에_PDF_하이라이팅_등록을_시도하면_실패한다() { + // given + List request = createPdfHighlightUpdateRequest(); + + // when & then + given(spec) + .body(request) + .when() + .put("/qna-set/" + otherUserQnaSetId + "/pdf-highlightings") + .then() + .statusCode(403) + .body("code", equalTo(INTERVIEW_NOT_ACCESSIBLE.name())) + .body("message", equalTo(INTERVIEW_NOT_ACCESSIBLE.getMessage())) + .body("result", nullValue()); + } + + + } + + @Nested + class PDF_하이라이팅_조회_시 { + + @Test + void 나의_빈_PDF_하이라이팅_정보_조회를_성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/" + qnaSetDraftQnaSetId + "/pdf-highlightings") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()); + } + + @Test + void 나의_데이터가_있는_PDF_하이라이팅_정보_조회를_성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/" + qnaSetWithPdfHighlightingId + "/pdf-highlightings") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()); + } + + @Test + void 회고_완료_면접의_나의_PDF_하이라이팅_정보_조회를_성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/" + debriefCompletedQnaSetId + "/pdf-highlightings") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()); + } + + @Test + void 다른_사람의_PDF_하이라이팅_정보_조회를_실패한다() { + given(spec) + .when() + .get("/qna-set/" + otherUserQnaSetId + "/pdf-highlightings") + .then() + .statusCode(403) + .body("code", equalTo(INTERVIEW_NOT_ACCESSIBLE.name())) + .body("message", equalTo(INTERVIEW_NOT_ACCESSIBLE.getMessage())) + .body("result", nullValue()); + } + } + + private List createPdfHighlightUpdateRequest() { + return List.of( + new PdfHighlightingUpdateRequest( + "highlighting1 text", + List.of( + new PdfHighlightingRectDto(3.14, 1.592, 30.12, 4123.432, 10), + new PdfHighlightingRectDto(0.0, 1.592, 34.0, 4123.432, 10), + new PdfHighlightingRectDto(3.14, 1.592, 30.12, 4123.432, 10), + new PdfHighlightingRectDto(3.14, 123123.1, 30.12, 4123.432, 1) + )), + new PdfHighlightingUpdateRequest( + "highlighting2 text", + List.of( + new PdfHighlightingRectDto(3.14, 1.592, 30.12, 4123.432, 1), + new PdfHighlightingRectDto(0.0, 1.592, 34.0, 4123.432, 2), + new PdfHighlightingRectDto(3.14, 1.592, 30.12, 4123.432, 3), + new PdfHighlightingRectDto(3.14, 123123.1, 30.12, 4123.432, 4) + )), + new PdfHighlightingUpdateRequest( + "highlighting3 text", + List.of( + new PdfHighlightingRectDto(3.14, 1.592, 30.12, 20201483.2, 13), + new PdfHighlightingRectDto(0.0, 1.592, 34.0, 4123.432, 13), + new PdfHighlightingRectDto(0.04, 1.592, 30.12, 4123.432, 13), + new PdfHighlightingRectDto(452.1, 123123.1, 30.12, 4123.432, 13) + )) + ); + } +} diff --git a/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetMyIntegrationTest.java b/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetMyIntegrationTest.java new file mode 100644 index 000000000..22f597f4b --- /dev/null +++ b/backend/src/test/java/com/shyashyashya/refit/integration/qnaset/QnaSetMyIntegrationTest.java @@ -0,0 +1,148 @@ +package com.shyashyashya.refit.integration.qnaset; + +import com.shyashyashya.refit.core.IntegrationTest; +import com.shyashyashya.refit.domain.interview.dto.request.InterviewCreateRequest; +import com.shyashyashya.refit.domain.interview.dto.request.QnaSetCreateRequest; +import com.shyashyashya.refit.domain.interview.model.Interview; +import com.shyashyashya.refit.domain.interview.model.InterviewReviewStatus; +import com.shyashyashya.refit.domain.interview.model.InterviewType; +import com.shyashyashya.refit.domain.qnaset.dto.request.QnaSetSearchRequest; +import com.shyashyashya.refit.domain.qnaset.model.QnaSet; +import com.shyashyashya.refit.domain.qnaset.model.StarInclusionLevel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.shyashyashya.refit.global.model.ResponseCode.COMMON200; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class QnaSetMyIntegrationTest extends IntegrationTest { + + @BeforeEach + void setUp() { + Interview interview = createAndSaveInterview( + new InterviewCreateRequest( + LocalDateTime.of(2023, 1, 10, 10, 0, 0), InterviewType.FIRST, company1.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.DEBRIEF_COMPLETED); + + // category가 null인 qnaSet 존재하면 NPE 발생 + // QnaSet qnaSet1 = createAndSaveQnaSet(new QnaSetCreateRequest("q text", "a text"), interview); + + QnaSet qnaSet2 = createAndSaveQnaSet( + new QnaSetCreateRequest("q text2", "a text"), + interview, + qnaSetCategory1); + + QnaSet qnaSet3 = createAndSaveQnaSet( + new QnaSetCreateRequest("q text3", "a text"), + interview, + qnaSetCategory1); + + QnaSet qnaSet4 = createAndSaveQnaSet( + new QnaSetCreateRequest("q text3", "a text"), + interview, + qnaSetCategory1); + + QnaSet qnaSet5 = createAndSaveQnaSet( + new QnaSetCreateRequest("q text3", "a text"), + interview, + qnaSetCategory3); + } + + @Nested + class 빈출_질문_카테고리_조회할_때 { + + @Test + void 성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/my/frequent/category") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()); + } + } + + @Nested + class 특정_카테고리_질문_조회할_때 { + + @Test + void 질문이_있는_카테고리의_조회를_성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/my/frequent/category/" + qnaSetCategory1.getId()) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()); + } + + @Test + void 질문이_없는_카테고리의_조회를_성공한다() { + // given + + // when & then + given(spec) + .when() + .get("/qna-set/my/frequent/category/" + qnaSetCategory2.getId()) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()) + .body("result.content", hasSize(0)); + } + } + + @Nested + class 나의_면접_질문_검색할_때 { + + @Test + void 모든_질문_검색을_성공한다() { + // given + Set inclusionLevels = new HashSet<>(); + inclusionLevels.add(StarInclusionLevel.NULL); + + QnaSetSearchRequest request = new QnaSetSearchRequest( + null, + new QnaSetSearchRequest.QnaSearchFilter( + null, + inclusionLevels, + inclusionLevels, + inclusionLevels, + inclusionLevels + ) + ); + + // when & then + given(spec) + .body(request) + .when() + .post("/qna-set/my/search") + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("message", equalTo(COMMON200.getMessage())) + .body("result", notNullValue()) + .body("result.content", hasSize(4)); + } + } +} diff --git a/backend/src/test/java/com/shyashyashya/refit/interview/integration/InterviewMyIntegrationTest.java b/backend/src/test/java/com/shyashyashya/refit/interview/integration/InterviewMyIntegrationTest.java index 3ce8a7c77..fb4059abe 100644 --- a/backend/src/test/java/com/shyashyashya/refit/interview/integration/InterviewMyIntegrationTest.java +++ b/backend/src/test/java/com/shyashyashya/refit/interview/integration/InterviewMyIntegrationTest.java @@ -232,7 +232,7 @@ void createTestData() { given(spec) .body(request) - .when() + .when() .post(path) .then() .statusCode(200) @@ -288,6 +288,81 @@ void createTestData() { } } + @Nested + class 나의_최근_한_달동안_기록을_시작하지_않은_면접_리스트를_조회할_때 { + + private final String path = "/interview/my/not-logged"; + + @Test + void 다른_사람의_면접은_조회되지_않는다() { + User otherUser = createAndSaveUser("other2@example.com", "other2", industry1, jobCategory1); + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.FIRST, company1.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.NOT_LOGGED, otherUser); + + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.FIRST, company2.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.NOT_LOGGED); + + given(spec) + .when() + .get(path) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("result", hasSize(1)) + .body("result[0].companyInfo.companyName", equalTo(company2.getName())); + } + + @Test + void NOT_LOGGED_상태의_면접만_조회된다() { + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.FIRST, company1.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.NOT_LOGGED); + + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.SECOND, company1.getName(), industry1.getId(), jobCategory1.getId(), "Engineer" + ), InterviewReviewStatus.LOG_DRAFT); + + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.THIRD, company1.getName(), industry1.getId(), jobCategory1.getId(), "Manager" + ), InterviewReviewStatus.QNA_SET_DRAFT); + + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now(), InterviewType.FIRST, company2.getName(), industry1.getId(), jobCategory1.getId(), "Manager" + ), InterviewReviewStatus.DEBRIEF_COMPLETED); + + given(spec) + .when() + .get(path) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("result", hasSize(1)) + .body("result[0].interviewReviewStatus", equalTo(InterviewReviewStatus.NOT_LOGGED.name())); + } + + @Test + void 최근_한달_이내의_면접만_조회된다() { + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now().minusDays(1), InterviewType.FIRST, company1.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.NOT_LOGGED); + + createAndSaveInterview(new InterviewCreateRequest( + LocalDateTime.now().minusMonths(2), InterviewType.FIRST, company2.getName(), industry1.getId(), jobCategory1.getId(), "Developer" + ), InterviewReviewStatus.NOT_LOGGED); + + given(spec) + .when() + .get(path) + .then() + .statusCode(200) + .body("code", equalTo(COMMON200.name())) + .body("result", hasSize(1)) + .body("result[0].companyInfo.companyName", equalTo(company1.getName())); + } + } + @Nested class 면접_임시저장_데이터를_조회할_때 { diff --git a/frontend/src/designs/assets/hour_glass_icon.svg b/frontend/src/designs/assets/hour_glass_icon.svg new file mode 100644 index 000000000..3184e1d56 --- /dev/null +++ b/frontend/src/designs/assets/hour_glass_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/designs/components/modal/index.tsx b/frontend/src/designs/components/modal/index.tsx index 61cc59ef9..8962c9c86 100644 --- a/frontend/src/designs/components/modal/index.tsx +++ b/frontend/src/designs/components/modal/index.tsx @@ -78,7 +78,7 @@ const Modal = ({ onClick={(e) => e.stopPropagation()} > {hasHeader && ( -
+
{title != null ? (

diff --git a/frontend/src/features/_common/auth/components/LiveAudioVisualizer.tsx b/frontend/src/features/_common/auth/components/LiveAudioVisualizer.tsx index 634982daa..3e91a00f8 100644 --- a/frontend/src/features/_common/auth/components/LiveAudioVisualizer.tsx +++ b/frontend/src/features/_common/auth/components/LiveAudioVisualizer.tsx @@ -1,4 +1,4 @@ -import type { Dispatch, SetStateAction } from 'react' +import { useEffect, type Dispatch, type SetStateAction } from 'react' import { MicIcon } from '@/designs/assets' import { Button } from '@/designs/components' import { useAudioRecorder } from '@/features/_common/auth/hooks' @@ -8,31 +8,59 @@ type LiveAudioVisualizerProps = { onComplete?: () => void /** STT 연동 시 실시간 인식 결과를 넘길 콜백 */ onRealtimeTranscript?: Dispatch> + uiType: 'mobile' | 'desktop' + /** 녹음 상태 변경 시 호출될 콜백 */ + onRecordingChange?: (isRecording: boolean) => void } -export default function LiveAudioVisualizer({ onCancel, onComplete, onRealtimeTranscript }: LiveAudioVisualizerProps) { +export default function LiveAudioVisualizer({ + onCancel, + onComplete, + onRealtimeTranscript, + uiType, + onRecordingChange, +}: LiveAudioVisualizerProps) { const { canvasRef, isRecording, timerText, isRequestingPermission, startRecording, cancel, complete } = useAudioRecorder({ onCancel, onComplete, onRealtimeTranscript }) + const isMobile = uiType === 'mobile' + + useEffect(() => { + onRecordingChange?.(isRecording) + }, [isRecording, onRecordingChange]) + return ( -
+
{!isRecording ? ( - + isMobile ? ( + + ) : ( + + ) ) : (
{timerText}
diff --git a/frontend/src/features/_common/components/sidebar/MinimizedSidebarLayoutSkeleton.tsx b/frontend/src/features/_common/components/sidebar/MinimizedSidebarLayoutSkeleton.tsx new file mode 100644 index 000000000..3288bc57f --- /dev/null +++ b/frontend/src/features/_common/components/sidebar/MinimizedSidebarLayoutSkeleton.tsx @@ -0,0 +1,19 @@ +export default function MinimizedSidebarLayoutSkeleton() { + return ( +
+
+ ) +} diff --git a/frontend/src/features/record/confirm/components/SidebarLayoutSkeleton.tsx b/frontend/src/features/_common/components/sidebar/SidebarLayoutSkeleton.tsx similarity index 100% rename from frontend/src/features/record/confirm/components/SidebarLayoutSkeleton.tsx rename to frontend/src/features/_common/components/sidebar/SidebarLayoutSkeleton.tsx diff --git a/frontend/src/features/_common/loading/LoadingOverlay.tsx b/frontend/src/features/_common/loading/LoadingOverlay.tsx new file mode 100644 index 000000000..4b343b5f5 --- /dev/null +++ b/frontend/src/features/_common/loading/LoadingOverlay.tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from 'react' +import HourGlassIcon from '@/designs/assets/hour_glass_icon.svg?react' + +type LoadingOverlayProps = { + text?: ReactNode +} + +export default function LoadingOverlay({ text }: LoadingOverlayProps) { + return ( +
+
+ + {text && {text}} +
+
+ ) +} diff --git a/frontend/src/features/dashboard/_index/components/review-waiting-interview/ReviewWaitingCard.tsx b/frontend/src/features/dashboard/_index/components/review-waiting-interview/ReviewWaitingCard.tsx index bbb20bccd..c49053662 100644 --- a/frontend/src/features/dashboard/_index/components/review-waiting-interview/ReviewWaitingCard.tsx +++ b/frontend/src/features/dashboard/_index/components/review-waiting-interview/ReviewWaitingCard.tsx @@ -1,6 +1,8 @@ +import { useNavigate } from 'react-router' import { SmallLogoIcon } from '@/designs/assets' import { Badge } from '@/designs/components' import Button from '@/designs/components/button' +import { ROUTES } from '@/routes/routes' export interface ReviewWaitingData { id: number @@ -17,6 +19,12 @@ interface ReviewWaitingCardProps { } export default function ReviewWaitingCard({ data }: ReviewWaitingCardProps) { + const navigate = useNavigate() + + const handleNavigate = () => { + navigate(ROUTES.RECORD.replace(':interviewId', String(data.id))) + } + return (
@@ -39,8 +47,13 @@ export default function ReviewWaitingCard({ data }: ReviewWaitingCardProps) {
-
) diff --git a/frontend/src/features/dashboard/_index/hooks/useReviewWaitingInterviews.ts b/frontend/src/features/dashboard/_index/hooks/useReviewWaitingInterviews.ts index 467d0d599..8aa0d321e 100644 --- a/frontend/src/features/dashboard/_index/hooks/useReviewWaitingInterviews.ts +++ b/frontend/src/features/dashboard/_index/hooks/useReviewWaitingInterviews.ts @@ -1,41 +1,46 @@ -import { useState } from 'react' +import { useGetDebriefIncompletedInterviews } from '@/apis/generated/dashboard-api/dashboard-api' +import { INTERVIEW_TYPE_LABEL } from '@/constants/interviews' import type { ReviewWaitingData } from '../components/review-waiting-interview/ReviewWaitingCard' -const MOCK_REVIEW_WAITING_DATA: ReviewWaitingData[] = [ - { - id: 1, - status: '기록 전', - elapsedText: '면접 끝난지 3일 지남', - companyName: '현대자동차', - industry: '제조업', - jobCategory: '데이터 사이언티스트', - interviewType: '컬쳐핏 면접', - }, - { - id: 2, - status: '기록 전', - elapsedText: '면접 끝난지 3일 지남', - companyName: '현대자동차', - industry: '제조업', - jobCategory: '데이터 사이언티스트', - interviewType: '컬쳐핏 면접', - }, - { - id: 3, - status: '기록 전', - elapsedText: '면접 끝난지 3일 지남', - companyName: '현대자동차', - industry: '제조업', - jobCategory: '데이터 사이언티스트', - interviewType: '컬쳐핏 면접', - }, -] - export const useReviewWaitingInterviews = () => { - const [data] = useState(MOCK_REVIEW_WAITING_DATA) + const { data: response } = useGetDebriefIncompletedInterviews( + { + pageable: { + page: 0, + size: 10, + }, + }, + { + query: { + select: (data) => ({ + content: data?.result?.content ?? [], + totalElements: data?.result?.totalElements ?? 0, + }), + }, + }, + ) + + // API 데이터가 없으면 빈 배열 반환 + const content = response?.content ?? [] + + const data: ReviewWaitingData[] = content.map((item) => { + const interview = item.interview + const interviewTypeKey = interview?.interviewType as keyof typeof INTERVIEW_TYPE_LABEL | undefined + return { + id: interview?.interviewId ?? 0, + status: '기록 전', + // passedDays가 0일 때 처리 (오늘 완료됨 등) 로직은 기획에 따라 다를 수 있음 + elapsedText: `면접 끝난지 ${item.passedDays ?? 0}일 지남`, + companyName: interview?.companyName ?? '', + // Industry 정보가 API에 없으므로 임시 하드코딩 또는 빈 문자열 + industry: interview?.companyName === '현대자동차' ? '제조업' : 'IT/플랫폼', + jobCategory: interview?.jobCategoryName ?? '', + interviewType: interviewTypeKey ? INTERVIEW_TYPE_LABEL[interviewTypeKey] : interview?.interviewType ?? '', + } + }) return { data, - count: data.length, + count: response?.totalElements ?? 0, } } diff --git a/frontend/src/features/dashboard/my-collections/components/FolderModal.tsx b/frontend/src/features/dashboard/my-collections/components/FolderModal.tsx index 1e728b8f5..be2767bf6 100644 --- a/frontend/src/features/dashboard/my-collections/components/FolderModal.tsx +++ b/frontend/src/features/dashboard/my-collections/components/FolderModal.tsx @@ -14,15 +14,6 @@ interface FolderModalProps { const FolderModal = ({ isOpen, onClose, onSubmit, initialName = '', title, submitLabel }: FolderModalProps) => { const [name, setName] = useState(initialName) - const [prevIsOpen, setPrevIsOpen] = useState(false) - - if (isOpen && (!prevIsOpen || name !== initialName)) { - setName(initialName) - setPrevIsOpen(true) - } - if (!isOpen && prevIsOpen) { - setPrevIsOpen(false) - } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() diff --git a/frontend/src/features/mobile/record/components/RecordStepContent.tsx b/frontend/src/features/mobile/record/components/RecordStepContent.tsx index f66b002e3..fc02433e9 100644 --- a/frontend/src/features/mobile/record/components/RecordStepContent.tsx +++ b/frontend/src/features/mobile/record/components/RecordStepContent.tsx @@ -30,6 +30,7 @@ export default function RecordStepContent({ onComplete={onRecordComplete} onRealtimeTranscript={onRealtimeTranscript} onCancel={onRecordCancel} + uiType="mobile" />
diff --git a/frontend/src/features/record/_index/components/RecordPageContent.tsx b/frontend/src/features/record/_index/components/RecordPageContent.tsx new file mode 100644 index 000000000..3c3a71271 --- /dev/null +++ b/frontend/src/features/record/_index/components/RecordPageContent.tsx @@ -0,0 +1,97 @@ +import { useState, type Dispatch, type SetStateAction } from 'react' +import { Button } from '@/designs/components' +import LiveAudioVisualizer from '@/features/_common/auth/components/LiveAudioVisualizer' +import { useInterviewNavigate } from '@/features/_common/hooks/useInterviewNavigation' +import LoadingOverlay from '@/features/_common/loading/LoadingOverlay' +import { RecordSidebar } from '@/features/record/_index/components/RecordSidebar' +import { ROUTES } from '@/routes/routes' +import type { InterviewInfoType } from '@/types/interview' + +type RecordPageContentProps = { + interviewInfo: InterviewInfoType + text: string + realtimeText: string + onTextChange: Dispatch> + onRealtimeTranscript: Dispatch> + onRecordComplete: () => void + onRecordCancel?: () => void + onSave: () => void + isSavePending: boolean + canSave: boolean +} + +export function RecordPageContent({ + interviewInfo, + text, + realtimeText, + onTextChange, + onRealtimeTranscript, + onRecordComplete, + onRecordCancel, + onSave, + isSavePending, + canSave, +}: RecordPageContentProps) { + const [isRecording, setIsRecording] = useState(false) + const [isSummarizing, setIsSummarizing] = useState(false) + + const navigateWithId = useInterviewNavigate() + const goToConfirmPage = () => { + setIsSummarizing(true) + setTimeout(() => { + navigateWithId(ROUTES.RECORD_CONFIRM) + }, 4000) + } + + return ( + <> +
+ +
+
+

면접 기록하기

+
+ +
+