From 8d6dda9bfcb584033f44a452d7f01c11fa65531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=ED=98=B8=EC=9C=A4?= Date: Sat, 16 Dec 2023 03:16:44 +0900 Subject: [PATCH] =?UTF-8?q?pdf=20=ED=8C=8C=EC=9D=BC=205=EC=9E=A5=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EC=B2=B4=ED=81=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=EC=84=9C,=20=EC=B1=84=EC=9A=A9=EA=B3=B5?= =?UTF-8?q?=EA=B3=A0,=20=EC=A7=88=EB=AC=B8=20=EC=83=9D=EC=84=B1=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C=20API=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20LOG=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=EC=A0=9C=EC=99=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../chwipoClova/common/entity/QApiLog.java | 45 ++++++++++++++++ .../common/config/WebSecurityConfig.java | 6 +-- .../com/chwipoClova/common/entity/ApiLog.java | 51 ++++++++++++++++++ .../common/exception/ExceptionCode.java | 8 ++- .../common/repository/ApiLogRepository.java | 8 +++ .../chwipoClova/common/utils/ApiUtils.java | 33 +++++++++++- .../interview/service/InterviewService.java | 8 +-- .../com/chwipoClova/qa/service/QaService.java | 54 ++++++++++++++----- .../recruit/service/RecruitService.java | 44 +++++++++++---- .../resume/service/ResumeService.java | 11 ++++ 11 files changed, 238 insertions(+), 33 deletions(-) create mode 100644 src/main/generated/com/chwipoClova/common/entity/QApiLog.java create mode 100644 src/main/java/com/chwipoClova/common/entity/ApiLog.java create mode 100644 src/main/java/com/chwipoClova/common/repository/ApiLogRepository.java diff --git a/build.gradle b/build.gradle index 6dfd81f..1b2a9a9 100644 --- a/build.gradle +++ b/build.gradle @@ -82,6 +82,9 @@ dependencies { implementation group: 'commons-io', name: 'commons-io', version: '2.15.1' implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.5' + //PDF + implementation group: 'org.apache.pdfbox', name: 'pdfbox', version: '3.0.0' + } diff --git a/src/main/generated/com/chwipoClova/common/entity/QApiLog.java b/src/main/generated/com/chwipoClova/common/entity/QApiLog.java new file mode 100644 index 0000000..84c5bbe --- /dev/null +++ b/src/main/generated/com/chwipoClova/common/entity/QApiLog.java @@ -0,0 +1,45 @@ +package com.chwipoClova.common.entity; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QApiLog is a Querydsl query type for ApiLog + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QApiLog extends EntityPathBase { + + private static final long serialVersionUID = -525634126L; + + public static final QApiLog apiLog = new QApiLog("apiLog"); + + public final NumberPath apiLogId = createNumber("apiLogId", Long.class); + + public final StringPath apiUrl = createString("apiUrl"); + + public final StringPath message = createString("message"); + + public final DateTimePath regDate = createDateTime("regDate", java.util.Date.class); + + public final NumberPath userId = createNumber("userId", Long.class); + + public QApiLog(String variable) { + super(ApiLog.class, forVariable(variable)); + } + + public QApiLog(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QApiLog(PathMetadata metadata) { + super(ApiLog.class, metadata); + } + +} + diff --git a/src/main/java/com/chwipoClova/common/config/WebSecurityConfig.java b/src/main/java/com/chwipoClova/common/config/WebSecurityConfig.java index 9e58e01..2b70b70 100644 --- a/src/main/java/com/chwipoClova/common/config/WebSecurityConfig.java +++ b/src/main/java/com/chwipoClova/common/config/WebSecurityConfig.java @@ -57,15 +57,11 @@ public SecurityFilterChain securityFilterChain(final @NotNull HttpSecurity http .authorizeHttpRequests(authorize -> authorize //.requestMatchers("/**").permitAll().anyRequest().authenticated() - .requestMatchers("/interview/**", "/resume/**", "/" + .requestMatchers("/" ,"/user/getKakaoUrl","/user/kakaoLogin","/user/kakaoCallback","/user/logout" ).permitAll().anyRequest().authenticated() - - //.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - - ) .addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class) .exceptionHandling((exception)-> exception.authenticationEntryPoint(new JwtAuthenticationEntryPoint())) diff --git a/src/main/java/com/chwipoClova/common/entity/ApiLog.java b/src/main/java/com/chwipoClova/common/entity/ApiLog.java new file mode 100644 index 0000000..98aedea --- /dev/null +++ b/src/main/java/com/chwipoClova/common/entity/ApiLog.java @@ -0,0 +1,51 @@ +package com.chwipoClova.common.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import java.util.Date; + +@Entity(name = "ApiLog") +@Table(name = "ApiLog") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties() +@DynamicInsert +@Builder +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "API 로그 VO") +public class ApiLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "apiLogId") + @Schema(description = "API LOG ID") + private Long apiLogId; + + @Column(name = "userId") + @Schema(description = "유저 ID") + private Long userId; + + @Column(name = "apiUrl") + @Schema(description = "api 호출 URL") + private String apiUrl; + + @Column(name = "message") + @Schema(description = "결과 메세지") + private String message; + + @Column(name = "regDate") + @Schema(description = "등록일") + private Date regDate; + + @PrePersist + public void prePersist() { + this.regDate = new Date(); // 현재 날짜와 시간으로 등록일 설정 + } +} diff --git a/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java b/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java index 29d9354..8a63727 100644 --- a/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java +++ b/src/main/java/com/chwipoClova/common/exception/ExceptionCode.java @@ -29,6 +29,8 @@ public enum ExceptionCode { FILE_SIZE("852", "파일 업로드 최대 크기는 50M 입니다."), + FILE_PDF_PAGE_OVER("853", "PDF 최대 장수를 넘었습니다."), + RESUME_NULL("860", "이력서 정보가 올바르지 않습니다."), RESUME_LIST_OVER("861", "이력서 최대 개수를 초과하였습니다."), @@ -37,6 +39,8 @@ public enum ExceptionCode { RECRUIT_CONTENT_NULL("870", "채용공고 정보가 올바르지 않습니다."), + RECRUIT_TITLE_NULL("871", "채용공고 기업명이 없습니다."), + INTERVIEW_NULL("880", "면접 정보가 올바르지 않습니다."), INTERVIEW_LIST_OVER("881", "면접 최대 개수를 초과하였습니다."), @@ -61,7 +65,9 @@ public enum ExceptionCode { API_RESUME_SUMMARY_NULL("984", "이력서 요약 결과가 비어있습니다."), - API_TOKEN_COUNT_FAIL("985", "토큰 계산기 호출 실패") + API_TOKEN_COUNT_FAIL("985", "토큰 계산기 호출 실패"), + + API_QA_NULL("986", "질문 API 결과가 비어있습니다.") ; diff --git a/src/main/java/com/chwipoClova/common/repository/ApiLogRepository.java b/src/main/java/com/chwipoClova/common/repository/ApiLogRepository.java new file mode 100644 index 0000000..ed293e1 --- /dev/null +++ b/src/main/java/com/chwipoClova/common/repository/ApiLogRepository.java @@ -0,0 +1,8 @@ +package com.chwipoClova.common.repository; + +import com.chwipoClova.common.dto.Token; +import com.chwipoClova.common.entity.ApiLog; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ApiLogRepository extends JpaRepository { +} diff --git a/src/main/java/com/chwipoClova/common/utils/ApiUtils.java b/src/main/java/com/chwipoClova/common/utils/ApiUtils.java index c0e01b6..bb19a65 100644 --- a/src/main/java/com/chwipoClova/common/utils/ApiUtils.java +++ b/src/main/java/com/chwipoClova/common/utils/ApiUtils.java @@ -1,7 +1,10 @@ package com.chwipoClova.common.utils; +import com.chwipoClova.common.dto.UserDetailsImpl; +import com.chwipoClova.common.entity.ApiLog; import com.chwipoClova.common.exception.CommonException; import com.chwipoClova.common.exception.ExceptionCode; +import com.chwipoClova.common.repository.ApiLogRepository; import com.chwipoClova.resume.response.ApiRes; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -13,7 +16,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; @@ -34,6 +40,8 @@ public class ApiUtils { private final RestTemplate restTemplate; + private final ApiLogRepository apiLogRepository; + @Value("${api.url.base}") private String apiBaseUrl; @@ -49,7 +57,6 @@ public class ApiUtils { @Value("${api.url.recruit}") private String recruit; - @Value("${api.url.question}") private String question; @@ -62,24 +69,47 @@ public class ApiUtils { @Value("${api.url.best}") private String best; + @Transactional public String callApi(URI apiUrl, HttpEntity entity) { String resultData = null; + String resultMessage = null; + Long userId = null; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication.getPrincipal() instanceof UserDetailsImpl) { + userId = ((UserDetailsImpl) authentication.getPrincipal()).getUser().getUserId(); + } try { ResponseEntity responseAsString = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class); if (responseAsString == null) { + resultMessage = "API 결과 NULL"; log.info("API 결과 NULL"); } else { if (responseAsString.getStatusCode() == HttpStatus.OK) { + resultMessage = "API 성공"; log.info("API 성공"); resultData = responseAsString.getBody(); } else { + resultMessage = "API 통신 결과 실패 HttpStatus" + responseAsString.getStatusCode(); log.error("API 통신 결과 실패 HttpStatus : {} ", responseAsString.getStatusCode()); } } } catch (Exception e) { + resultMessage = "callApi 실패 error " + e.getMessage(); log.error("callApi 실패 error : {}", e.getMessage()); } + if (resultData == null) { + resultMessage = ExceptionCode.API_NULL.getMessage(); + } + + // API 로그 적재 + ApiLog apiLog = ApiLog.builder() + .userId(userId) + .apiUrl(apiUrl.toString()) + .message(resultMessage) + .build(); + apiLogRepository.save(apiLog); + if (resultData == null) { throw new CommonException(ExceptionCode.API_NULL.getMessage(), ExceptionCode.API_NULL.getCode()); } @@ -176,7 +206,6 @@ public String question(String recruitSummary, String resumeSummary) { body.add("recruit_summary", recruitSummary); body.add("resume_summary", resumeSummary); - HttpEntity> requestEntity = new HttpEntity<>(body, httpHeaders); URI apiUrl = UriComponentsBuilder .fromHttpUrl(apiBaseUrl + question) diff --git a/src/main/java/com/chwipoClova/interview/service/InterviewService.java b/src/main/java/com/chwipoClova/interview/service/InterviewService.java index 6f9b578..edbd174 100644 --- a/src/main/java/com/chwipoClova/interview/service/InterviewService.java +++ b/src/main/java/com/chwipoClova/interview/service/InterviewService.java @@ -97,7 +97,6 @@ public InterviewInsertRes insertInterview(InterviewInsertReq interviewInsertReq, .build(); Interview interviewRst = interviewRepository.save(interview); - // TODO 이력서 요약과 채용공고 요약을 이용해서 질문, AI 답변 생성 List questionData = qaService.insertQa(interviewRst); return InterviewInsertRes.builder() @@ -216,12 +215,12 @@ public void downloadInterview(Long userId, Long interviewId, HttpServletResponse stringBuilder.append("면접 관의 속마음"); stringBuilder.append("\n"); - stringBuilder.append(feedback); + stringBuilder.append(feedback == null ? "" : feedback); stringBuilder.append("\n"); stringBuilder.append("\n"); - stringBuilder.append("티키타카의 피드백"); + stringBuilder.append("티키타카 피드백"); stringBuilder.append("\n"); stringBuilder.append("\n"); @@ -233,6 +232,9 @@ public void downloadInterview(Long userId, Long interviewId, HttpServletResponse stringBuilder.append(qaListForFeedbackRes.getQuestion()); stringBuilder.append("\n"); + stringBuilder.append(qaListForFeedbackRes.getAnswer()); + stringBuilder.append("\n"); + stringBuilder.append(qaListForFeedbackRes.getBestAnswer()); stringBuilder.append("\n"); }); diff --git a/src/main/java/com/chwipoClova/qa/service/QaService.java b/src/main/java/com/chwipoClova/qa/service/QaService.java index ce89a12..a93fa61 100644 --- a/src/main/java/com/chwipoClova/qa/service/QaService.java +++ b/src/main/java/com/chwipoClova/qa/service/QaService.java @@ -4,6 +4,7 @@ import com.chwipoClova.common.exception.ExceptionCode; import com.chwipoClova.common.response.CommonResponse; import com.chwipoClova.common.response.MessageCode; +import com.chwipoClova.common.utils.ApiUtils; import com.chwipoClova.feedback.request.FeedbackInsertReq; import com.chwipoClova.feedback.service.FeedbackService; import com.chwipoClova.interview.entity.Interview; @@ -50,6 +51,8 @@ public class QaService { private final UserRepository userRepository; + private final ApiUtils apiUtils; + @Transactional public List insertQaQuestionList(List qaQuestionInsertReqList) throws IOException { List qaList = new ArrayList<>(); @@ -120,6 +123,14 @@ public CommonResponse insertAnswer(QaAnswerInsertReq qaAnswerInsertReq) throws I // 마지막 답변이 있을 경우 면접 완료 처리 및 피드백 생성 Boolean lastCk = lastCkAtomic.get(); if (lastCk) { + // 면접관의 속마음 + //apiUtils.feel(); + + // 키워드 + + + // 모범답안 + // 피드백 요청 및 등록 feedbackService.insertFeedback(feedbackInsertListReq); @@ -244,22 +255,39 @@ public void generateQa(QaGenerateReq qaGenerateReq) throws IOException { } public List insertQa(Interview interviewRst) throws IOException { - String time = LocalDateTime.now().format(DateTimeFormatter.ISO_TIME); - String q1 = "질문1입니다." + time; + String recruitSummary = interviewRst.getRecruitSummary(); + String resumeSummary = interviewRst.getResumeSummary(); - String q2 = "질문2입니다." + time; + String apiQaRst = apiUtils.question(recruitSummary, resumeSummary); - // 질문 답변 저장 List qaQuestionInsertReqList = new ArrayList<>(); - QaQuestionInsertReq qaQuestionInsertReq1 = new QaQuestionInsertReq(); - qaQuestionInsertReq1.setInterview(interviewRst); - qaQuestionInsertReq1.setQuestion(q1); - qaQuestionInsertReqList.add(qaQuestionInsertReq1); - - QaQuestionInsertReq qaQuestionInsertReq2 = new QaQuestionInsertReq(); - qaQuestionInsertReq2.setInterview(interviewRst); - qaQuestionInsertReq2.setQuestion(q2); - qaQuestionInsertReqList.add(qaQuestionInsertReq2); + getApiQaList(apiQaRst).stream().forEach(apiQa -> { + QaQuestionInsertReq qaQuestionInsertReq = new QaQuestionInsertReq(); + qaQuestionInsertReq.setInterview(interviewRst); + qaQuestionInsertReq.setQuestion(apiQa); + qaQuestionInsertReqList.add(qaQuestionInsertReq); + }); return insertQaQuestionList(qaQuestionInsertReqList); } + + private List getApiQaList(String apiQaRst) { + // 현재 사용하지 않는 --- 제거 및 줄바꿈 분리 + String[] splitSummaryList = apiQaRst.split("\n"); + + List apiQaList = new ArrayList<>(); + for (String splitSummary : splitSummaryList) { + if (splitSummary.indexOf(".") != -1) { + String num = splitSummary.substring(0, splitSummary.indexOf(".")); + if (org.apache.commons.lang3.StringUtils.isNumeric(num)) { + apiQaList.add(splitSummary.trim()); + } + } + } + + if (apiQaList.size() == 0) { + throw new CommonException(ExceptionCode.API_QA_NULL.getMessage(), ExceptionCode.API_QA_NULL.getCode()); + } + + return apiQaList; + } } diff --git a/src/main/java/com/chwipoClova/recruit/service/RecruitService.java b/src/main/java/com/chwipoClova/recruit/service/RecruitService.java index acb95f3..1ca4534 100644 --- a/src/main/java/com/chwipoClova/recruit/service/RecruitService.java +++ b/src/main/java/com/chwipoClova/recruit/service/RecruitService.java @@ -89,12 +89,10 @@ public RecruitInsertRes insertRecruit(RecruitInsertReq recruitInsertReq) throws apiUtils.countTokenLimitCk(recruitContent, apiBaseTokenLimit); // 채용공고 요약 실행 - //String summary = apiUtils.summaryRecruit(recruitContent); + String summary = apiUtils.summaryRecruit(recruitContent); - String summary = recruitContent + "요약"; - - // TODO 채용 공고 제목 추출 - String title = recruitContent + "제목"; + // 채용공고에서 제목 추출 + String title = getRecruitTitle(summary); recruit = Recruit.builder() .title(title) @@ -143,10 +141,10 @@ public RecruitInsertRes insertRecruit(RecruitInsertReq recruitInsertReq) throws apiUtils.countTokenLimitCk(resumeTxt, apiBaseTokenLimit); // 채용공고 요약 - //String summary = apiUtils.summaryRecruit(resumeTxt); - String summary = recruitContent + "요약"; - // TODO 채용 공고 제목 추출 - String title = originalName + "제목"; + String summary = apiUtils.summaryRecruit(recruitContent); + + // 채용공고에서 제목 추출 + String title = getRecruitTitle(summary); recruit = Recruit.builder() .title(title) @@ -169,6 +167,34 @@ public RecruitInsertRes insertRecruit(RecruitInsertReq recruitInsertReq) throws return recruitInsertRes; } + + private String getRecruitTitle(String summary) { + String title = null; + // 현재 사용하지 않는 --- 제거 및 줄바꿈 분리 + if (summary.indexOf("---") != -1) { + String[] splitSummaryList = summary.substring(0, summary.lastIndexOf("---")).split("\n"); + + // 기업명 고정 + String targetWord = "기업 :"; + for (String splitSummary : splitSummaryList) { + if (splitSummary.indexOf(".") != -1) { + String num = splitSummary.substring(0, splitSummary.indexOf(".")); + if (org.apache.commons.lang3.StringUtils.isNumeric(num)) { + int index = splitSummary.indexOf(targetWord); + if (index != -1) { + title = splitSummary.substring(index + targetWord.length()).trim(); + } + } + } + } + } + + if (org.apache.commons.lang3.StringUtils.isBlank(title)) { + throw new CommonException(ExceptionCode.RECRUIT_TITLE_NULL.getMessage(), ExceptionCode.RECRUIT_TITLE_NULL.getCode()); + } + return title; + } + private String makeFolder() { String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); diff --git a/src/main/java/com/chwipoClova/resume/service/ResumeService.java b/src/main/java/com/chwipoClova/resume/service/ResumeService.java index 5a13f18..81be7bb 100644 --- a/src/main/java/com/chwipoClova/resume/service/ResumeService.java +++ b/src/main/java/com/chwipoClova/resume/service/ResumeService.java @@ -20,6 +20,8 @@ import jakarta.xml.bind.Unmarshaller; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; @@ -118,6 +120,15 @@ public ResumeUploadRes uploadResume(Long userId, MultipartFile file) throws IOEx Path savePath = Paths.get(saveName); file.transferTo(savePath); + File pdfFile = new File(saveName); + PDDocument document = Loader.loadPDF(pdfFile); + int pageCount = document.getNumberOfPages(); + + if (pageCount > 5) { + pdfFile.delete(); + throw new CommonException(ExceptionCode.FILE_PDF_PAGE_OVER.getMessage(), ExceptionCode.FILE_PDF_PAGE_OVER.getCode()); + } + // 이력서 OCR String resumeTxt = apiUtils.ocr(file);