Skip to content
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package UMC.career_mate.domain.answer.repository;

import UMC.career_mate.domain.answer.Answer;
import UMC.career_mate.domain.job.Job;
import UMC.career_mate.domain.member.Member;
import UMC.career_mate.domain.question.Question;
import UMC.career_mate.domain.template.enums.TemplateType;
Expand All @@ -12,18 +13,35 @@
import java.util.Optional;

public interface AnswerRepository extends JpaRepository<Answer, Long> {

@Query("""
SELECT a FROM Answer a
WHERE a.member = :member AND a.question.template.templateType = :templateType
""")
List<Answer> findByMemberAndTemplateType(@Param("member") Member member, @Param("templateType") TemplateType templateType);
SELECT a FROM Answer a
WHERE a.member = :member AND a.question.template.templateType = :templateType
""")
List<Answer> findByMemberAndTemplateType(@Param("member") Member member,
@Param("templateType") TemplateType templateType);

Optional<Answer> findByMemberAndQuestionAndSequence(Member member, Question question, Long sequence);
Optional<Answer> findByMemberAndQuestionAndSequence(Member member, Question question,
Long sequence);

@Query("""
SELECT COUNT(a) > 0 FROM Answer a
WHERE a.member = :member AND a.question.template.templateType = :templateType
""")
boolean existsByMemberAndTemplateType(@Param("member") Member member, @Param("templateType") TemplateType templateType);
SELECT COUNT(a) > 0 FROM Answer a
WHERE a.member = :member AND a.question.template.templateType = :templateType
""")
boolean existsByMemberAndTemplateType(@Param("member") Member member,
@Param("templateType") TemplateType templateType);

@Query("select a from Answer a where a.member = :member and a.question.template.templateType in :templateTypes and a.question.order = :order and a.sequence = :sequence and a.question.template.job = :job")
List<Answer> findByMemberAndTemplateTypesAndOrderAndJob(@Param("member") Member member,
@Param("templateTypes") List<TemplateType> templateTypes, @Param("order") int order,
@Param("sequence") int sequence, @Param("job") Job job);

@Query("select a from Answer a where a.member = :member and a.question.content = :content and a.question.template.templateType = :templateType and a.question.template.job = :job")
List<Answer> findAnswersByMemberAndQuestionContentAndTemplateTypeAndJob(
@Param("member") Member member, @Param("content") String content,
@Param("templateType") TemplateType templateType, @Param("job") Job job);

@Query("select a from Answer a where a.member = :member and a.question.template.templateType in :templateTypes and a.question.template.job = :job")
List<Answer> findByMemberAndTemplateTypesAndJob(@Param("member") Member member,
@Param("templateTypes") List<TemplateType> templateTypes, @Param("job") Job job);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public void updateAnswerList(Member member, AnswerCreateOrUpdateDTO request) {
.orElseThrow(() -> new GeneralException(CommonErrorCode.NOT_FOUND_ANSWER));

existingAnswer.updateContent(answerInfo.content());

// 질문 order 1의 답변 sequence 1은 수정일을 매번 업데이트 -> recruit 조회 로직에서 사용
if (question.getOrder() == 1 && existingAnswer.getSequence() == 1) {
existingAnswer.setUpdatedAt();
}
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/UMC/career_mate/domain/chatgpt/GptAnswer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package UMC.career_mate.domain.chatgpt;

import UMC.career_mate.domain.member.Member;
import UMC.career_mate.domain.recruit.enums.RecruitKeyword;
import UMC.career_mate.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;

@Entity
@Table(name = "gpt_answers")
@SQLRestriction("deleted_at is NULL")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class GptAnswer extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "gpt_answer_id")
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

@Enumerated(EnumType.STRING)
private RecruitKeyword recruitKeyword;

private int careerYear;

public void updateData(RecruitKeyword recruitKeyword, int careerYear) {
this.recruitKeyword = recruitKeyword;
this.careerYear = careerYear;
super.setUpdatedAt();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package UMC.career_mate.domain.chatgpt.converter;

import UMC.career_mate.domain.chatgpt.GptAnswer;
import UMC.career_mate.domain.member.Member;
import UMC.career_mate.domain.recruit.enums.RecruitKeyword;

public class GptAnswerConverter {

public static GptAnswer toEntity(Member member, RecruitKeyword recruitKeyword, int careerYear) {
return GptAnswer.builder()
.member(member)
.recruitKeyword(recruitKeyword)
.careerYear(careerYear)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package UMC.career_mate.domain.chatgpt.repository;

import UMC.career_mate.domain.chatgpt.GptAnswer;
import UMC.career_mate.domain.member.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface GptAnswerRepository extends JpaRepository<GptAnswer, Long> {

Optional<GptAnswer> findByMember(Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import UMC.career_mate.domain.chatgpt.dto.api.response.ChatCompletionResponse;
import UMC.career_mate.domain.chatgpt.dto.request.ChatGPTRequestDTO;
import UMC.career_mate.domain.chatgpt.dto.response.FilterConditionDTO;
import UMC.career_mate.domain.chatgpt.dto.api.request.GptRequest;
import UMC.career_mate.domain.chatgpt.dto.api.request.GptRequest.Message;
import UMC.career_mate.domain.recruit.enums.RecruitKeyword;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.util.Collections;
Expand All @@ -32,29 +30,48 @@ public class ChatGptService {
@Value("${chat-gpt.service-key}")
private String serviceKey;

private static final String GPT_REQUEST_FORMAT_PREFIX_FOR_CAREER_YEAR =
"다음 이력서 데이터를 기반으로 추천 직무와 총 경력을 계산해줘. " +
"직무는 'BACKEND_SPRING', 'BACKEND_NODE', 'BACKEND_DJANGO', 'FRONTEND', 'DESIGNER', 'PM' 중 하나로 선택하고, " +
"총 경력은 각 회사의 근무 기간을 더해서 소수 말고 정수로만 결과를 계산해줘. " +
"응답은 JSON으로 {\"직무\": \"추천 직무\", \"경력\": \"총 경력(년차)\"} 형식으로 작성해줘.";
private static final String GPT_REQUEST_FORMAT_POSTFIX_FOR_CAREER_YEAR =
"이 사람의 경력을 계산해서 앞뒤 설명 하지말고 정수로만 올바른 답변 예시와 같은 형식으로 답변해줘. " +
"올바론 답변 예시) 5, 잘못된 답변 예시 1) 5년, 잘못된 답변 예시 2) 5 years, 잘못된 답변 예시 3) 이 사람의 경력은 5년";

private static final String GPT_REQUEST_FORMAT_POSTFIX_FOR_RECRUIT_KEYWORD =
"이 사람의 직무를 내가 제시한 보기들 중에서 하나만 골라서 답변해줘.\n" +
"직무 보기 :'BACKEND', 'BACKEND_SPRING', 'BACKEND_NODE', 'BACKEND_DJANGO', 'FRONTEND', 'DESIGNER', 'PM', "
+
"이 중에서 적절한 직무가 없다면 'MISMATCH'로 답변해줘.\n" +
"앞뒤 설명하지 말고 직무만 답변해줘.";

private static final String GPT_REQUEST_FORMAT_PREFIX_FOR_COMMENT =
" = 사용자 이름이고, 다음 이력서 데이터를 기반으로 사용자의 강점을 어필할 수 있고, " +
"어떤 포지션이 어울리는지 어떤 경험을 어필하면 좋을지 그런 내용들로 추천 조언 문구를 생성해줘. " +
"'~~한 경험이 있는 000님, ~~한 경험을 어필해보면 어때요?', 또는 '~~포지에 지원해보 건 어떨까요? ~~에 강점을 드러낼 수 있을 것 같아요.'" +
"와 비슷한 형식이지만 꼭 이런 형식이 아니더라도 너만의 스타일로 문구를 생성해줘." +
"말투는 \"입니다\"같은 딱딱말 말투는 사용하지 말고, \"요.\"같이 부드럽게 표현해줘. " +
"답변은 문구만 답변해줘. 문구를 생성할 때 이력서 데이터의 회사 이름은 제외해줘. 답변에서 엔터나 - 같은건 빼줘.";
"어떤 포지션이 어울리는지 어떤 경험을 어필하면 좋을지 그런 내용들로 추천 조언 문구를 생성해줘. " +
"'~~한 경험이 있는 000님, ~~한 경험을 어필해보면 어때요?', 또는 '~~포지에 지원해보 건 어떨까요? ~~에 강점을 드러낼 수 있을 것 같아요.'"
+
"와 비슷한 형식이지만 꼭 이런 형식이 아니더라도 너만의 스타일로 문구를 생성해줘." +
"말투는 \"입니다\"같은 딱딱말 말투는 사용하지 말고, \"요.\"같이 부드럽게 표현해줘. " +
"답변은 문구만 답변해줘. 문구를 생성할 때 이력서 데이터의 회사 이름은 제외해줘. 답변에서 엔터나 - 같은건 빼줘.";

public int getCareerYear(String chatGptRequestContent) {
GptRequest gptRequest = createGptRequest(
chatGptRequestContent + GPT_REQUEST_FORMAT_POSTFIX_FOR_CAREER_YEAR);

ObjectMapper om = new ObjectMapper();
String gptAnswer = getGptAnswer(om, gptRequest);

public FilterConditionDTO getFilterCondition(ChatGPTRequestDTO chatGPTRequestDTO) {
log.info("\ngptRequest : {}, gptAnswer : {} ", gptRequest, gptAnswer);

return Integer.parseInt(gptAnswer);
}

public RecruitKeyword getRecruitKeyword(String chatGptRequestContent) {
GptRequest gptRequest = createGptRequest(
GPT_REQUEST_FORMAT_PREFIX_FOR_CAREER_YEAR + " " + chatGPTRequestDTO.content());
chatGptRequestContent + GPT_REQUEST_FORMAT_POSTFIX_FOR_RECRUIT_KEYWORD);

ObjectMapper om = new ObjectMapper();
String gptAnswer = getGptAnswer(om, gptRequest);

// "직무"와 "경력" 추출
return extractJobAndCareer(om, gptAnswer);
log.info("\ngptRequest : {}, gptAnswer : {} ", gptRequest, gptAnswer);

return RecruitKeyword.getRecruitKeywordFromGptAnswerJob(gptAnswer);
}

public String getComment(ChatGPTRequestDTO chatGPTRequestDTO) {
Expand Down Expand Up @@ -153,28 +170,4 @@ private void logGptUsage(ChatCompletionResponse.Usage usage) {
private String parseGptResponse(ChatCompletionResponse response) {
return response.choices().get(0).message().content();
}

private FilterConditionDTO extractJobAndCareer(ObjectMapper om, String gptAnswer) {
JsonNode rootNode = null;
try {
rootNode = om.readTree(gptAnswer);
} catch (JsonProcessingException e) {
log.error("직무 경력 gpt 답변 결과 파싱 중 에러");
throw new RuntimeException(e);
}

String job = rootNode.get("직무").asText();
String career = rootNode.get("경력").asText();
int careerYear = Integer.parseInt(career.split("년")[0]);

log.info("추천 직무: {}", job);
log.info("추천 경력: {}", career);

RecruitKeyword recruitKeyword = RecruitKeyword.getRecruitKeyword(job);

return FilterConditionDTO.builder()
.recruitKeyword(recruitKeyword)
.careerYear(careerYear)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package UMC.career_mate.domain.chatgpt.service;

import UMC.career_mate.domain.chatgpt.GptAnswer;
import UMC.career_mate.domain.chatgpt.converter.GptAnswerConverter;
import UMC.career_mate.domain.chatgpt.repository.GptAnswerRepository;
import UMC.career_mate.domain.member.Member;
import UMC.career_mate.domain.recruit.enums.RecruitKeyword;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.REQUIRES_NEW)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 무슨 어노테이션인가요??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

트랜잭션 전파 설정을 정해주는 옵션입니다! GptAnswerCommandService의 메서드들(save, update)을 호출하는 쪽(RecruitQueryService)이 readOnly 상태라서 해당 트랜잭션이 이어지면 db 변경이 불가능해서, 전파 옵션으로 트랜잭션을 분리했습니다

public class GptAnswerCommandService {

private final GptAnswerRepository gptAnswerRepository;

public void saveGptAnswer(Member member, RecruitKeyword recruitKeyword, int careerYear) {
GptAnswer gptAnswer = GptAnswerConverter.toEntity(member, recruitKeyword, careerYear);
gptAnswerRepository.save(gptAnswer);
}

public void updateGptAnswer(GptAnswer gptAnswer, RecruitKeyword recruitKeyword, int careerYear) {
gptAnswer.updateData(recruitKeyword, careerYear);
gptAnswerRepository.save(gptAnswer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,20 @@ public ApiResponse<PageResponseDTO<List<RecommendRecruitDTO>>> getRecommendRecru
@RequestParam(defaultValue = "6", required = false) int size,
@RequestParam(defaultValue = "POSTING_DESC", required = false) RecruitSortType recruitSortType,
@LoginMember Member member
) {
) {
return ApiResponse.onSuccess(
recruitQueryService.getRecommendRecruitList(page, size, recruitSortType, member));
}

@Operation(
summary = "채용 공고 요약 페이지 조회 API",
description = """
채용 공고 요약 페이지를 조회하는 API입니다.\n\n
recruitId : 조회하려는 채용 공고 pk 값
""")
채용 공고 요약 페이지를 조회하는 API입니다.\n\n
recruitId : 조회하려는 채용 공고 pk 값
""")
@GetMapping("/{recruitId}")
public ResponseEntity<ApiResponse<RecruitInfoDTO>> getRecruitInfo(@PathVariable Long recruitId, @LoginMember Member member) {
public ResponseEntity<ApiResponse<RecruitInfoDTO>> getRecruitInfo(@PathVariable Long recruitId,
@LoginMember Member member) {
return ResponseEntity.ok(
ApiResponse.onSuccess(recruitQueryService.findRecruitInfo(member, recruitId)));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package UMC.career_mate.domain.recruit.converter;

import UMC.career_mate.domain.recruit.Recruit;
import UMC.career_mate.domain.recruit.dto.FilterConditionDTO;
import UMC.career_mate.domain.recruit.dto.api.SaraminResponseDTO.Job;
import UMC.career_mate.domain.recruit.dto.response.RecommendRecruitDTO;
import UMC.career_mate.domain.recruit.dto.response.RecruitInfoDTO;
import UMC.career_mate.domain.recruit.enums.EducationLevel;
import UMC.career_mate.domain.recruit.enums.RecruitKeyword;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -78,6 +80,13 @@ public static RecruitInfoDTO toRecruitInfoDTO(String comment, Recruit recruit) {
.build();
}

public static FilterConditionDTO toFilterConditionDTO(RecruitKeyword recruitKeyword, int careerYear) {
return FilterConditionDTO.builder()
.recruitKeyword(recruitKeyword)
.careerYear(careerYear)
.build();
}

private static String formatDeadLine(Recruit recruit) {
LocalDate today = LocalDate.now();
LocalDate targetDate = recruit.getDeadLine().toLocalDate();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package UMC.career_mate.domain.chatgpt.dto.response;
package UMC.career_mate.domain.recruit.dto;

import UMC.career_mate.domain.recruit.enums.RecruitKeyword;
import lombok.Builder;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
package UMC.career_mate.domain.recruit.enums;

import UMC.career_mate.domain.job.Job;
import java.util.List;

public enum RecruitKeyword {

BACKEND {
@Override
public List<String> getIncludeKeywordList() {
return List.of("Back", "Backend", "Back-end", "BACK", "백엔드", "서버", "시스템");
}

@Override
public List<String> getExcludeKeywordList() {
return null;
}
}
,
BACKEND_SPRING {
@Override
public List<String> getIncludeKeywordList() {
Expand Down Expand Up @@ -75,13 +88,23 @@ public List<String> getExcludeKeywordList() {

;

public static RecruitKeyword getRecruitKeyword(String name) {
public static RecruitKeyword getRecruitKeywordFromGptAnswerJob(String name) {
for (RecruitKeyword recruitKeyword : RecruitKeyword.values()) {
if (recruitKeyword.toString().equals(name)) {
return recruitKeyword;
}
}
return null; // 일치하는거 없으면 일단 Null 반환 처리
return null; // 일치하는거 없으면 null
}

public static RecruitKeyword getRecruitKeywordFromProfileJob(Job job) {
return switch (job.getName()) {
case "백엔드 개발자" -> BACKEND;
case "프론트엔드 개발자" -> FRONTEND;
case "PM(Product/Project Manager)" -> PM;
case "Designer" -> DESIGNER;
default -> throw new RuntimeException("추가 필요한 직무" + job.getName());
};
}


Expand Down
Loading