Skip to content

Commit 0cab960

Browse files
authored
#106 Feat: 로드맵 채팅 시 작성하는 자격증을 사용자 맞춤으로 추천해주는 API를 추가한다 (#107)
* #106 Feat: 로드맵 채팅 시 자격증 추천 API를 추가한다 * #106 Feat: 사용자 맞춤형 AI 자격증 request를 추가한다 * #106 Feat: 사용자 맞춤형 AI 자격증 API의 Http 메소드를 POST로 변경한다
1 parent 521f943 commit 0cab960

File tree

5 files changed

+124
-0
lines changed

5 files changed

+124
-0
lines changed

src/main/java/next/career/domain/openai/service/OpenAiService.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,4 +557,77 @@ private Map<String, Object> setStrengthReportPrompt(String system) {
557557
);
558558
}
559559

560+
public List<String> getCertificationList(String occupation) {
561+
562+
List<Prompt> certification = promptRepository.findAllByTag("certification");
563+
564+
String system = certification.stream()
565+
.map(Prompt::getContent)
566+
.filter(Objects::nonNull)
567+
.map(String::trim)
568+
.filter(s -> !s.isEmpty())
569+
.collect(Collectors.joining("\n"));
570+
571+
String finalSystemPrompt = system + "\n\n[사용자 희망 직무]\n" + occupation;
572+
573+
Map<String, Object> body = setCertificationPrompt(finalSystemPrompt);
574+
575+
try {
576+
Map res = requestOpenAI(body);
577+
log.info("res = {}", res);
578+
579+
List<Map<String, Object>> choices = (List<Map<String, Object>>) res.get("choices");
580+
581+
String content = getContent(choices);
582+
583+
RoadmapDto.CertificationResponse dto =
584+
MAPPER.readValue(content, RoadmapDto.CertificationResponse.class);
585+
586+
return Optional.ofNullable(dto.getCertificationList()).orElseGet(List::of)
587+
.stream()
588+
.filter(Objects::nonNull)
589+
.map(String::trim)
590+
.filter(s -> !s.isEmpty())
591+
.distinct()
592+
.limit(10)
593+
.toList();
594+
595+
} catch (Exception e) {
596+
throw new CoreException(GlobalErrorType.GET_CERTIFICATION_ERROR);
597+
}
598+
}
599+
600+
private Map<String, Object> setCertificationPrompt(String system) {
601+
602+
List<Map<String, Object>> messages = new ArrayList<>();
603+
604+
if (!system.isBlank()) {
605+
messages.add(Map.of("role", "system", "content", system));
606+
}
607+
608+
Map<String, Object> responseFormat = Map.of(
609+
"type", "json_schema",
610+
"json_schema", Map.of(
611+
"name", "certification_response",
612+
"schema", Map.of(
613+
"type", "object",
614+
"properties", Map.of(
615+
"certificationList", Map.of(
616+
"type", "array",
617+
"items", Map.of("type", "string")
618+
)
619+
),
620+
"required", List.of("certificationList")
621+
)
622+
)
623+
);
624+
625+
return Map.of(
626+
"model", "gpt-4o",
627+
"messages", messages,
628+
"temperature", 0.5,
629+
"max_tokens", 800,
630+
"response_format", responseFormat
631+
);
632+
}
560633
}

src/main/java/next/career/domain/roadmap/controller/RoadmapController.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,30 @@ public ApiResponse<RoadmapDto.RoadmapActionRecommendResponse> recommendRoadmapAc
177177
return ApiResponse.success(response);
178178
}
179179

180+
@PostMapping("/certification")
181+
@Operation(
182+
summary = "사용자 맞춤형 AI 자격증 추천",
183+
description = """
184+
사용자의 직무 정보를 입력받아,
185+
해당 직무에 적합한 자격증 목록을 AI 모델을 통해 추천합니다.
186+
187+
예시 요청:
188+
{
189+
"occupation": "백엔드 개발자"
190+
}
191+
"""
192+
)
193+
public ApiResponse<RoadmapDto.CertificationResponse> getCertifications(
194+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
195+
description = "자격증 추천 요청 DTO (occupation 필드 필수)",
196+
required = true
197+
)
198+
@RequestBody RoadmapDto.CertificationRequest request
199+
) {
200+
List<String> certifications = roadmapService.getCertifications(request.getOccupation());
201+
RoadmapDto.CertificationResponse response = RoadmapDto.CertificationResponse.of(certifications);
202+
return ApiResponse.success(response);
203+
}
180204

181205

182206
}

src/main/java/next/career/domain/roadmap/controller/dto/RoadmapDto.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,26 @@ public static class RoadMapUpdateRequest {
4747
private String period;
4848
private String experience;
4949
}
50+
51+
@Getter
52+
@NoArgsConstructor
53+
@AllArgsConstructor
54+
@Builder
55+
public static class CertificationResponse {
56+
private List<String> certificationList;
57+
58+
public static CertificationResponse of(List<String> certificationList) {
59+
return CertificationResponse.builder()
60+
.certificationList(certificationList)
61+
.build();
62+
}
63+
}
64+
65+
@Getter
66+
@NoArgsConstructor
67+
@AllArgsConstructor
68+
public static class CertificationRequest{
69+
private String occupation;
70+
}
71+
5072
}

src/main/java/next/career/domain/roadmap/service/RoadmapService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,8 @@ public void updateRoadmapInput(RoadmapDto.RoadMapUpdateRequest request, Member m
141141

142142
roadmapInput.update(request.getCareer(), request.getPeriod(), request.getExperience());
143143
}
144+
145+
public List<String> getCertifications(String occupation) {
146+
return openAiService.getCertificationList(occupation);
147+
}
144148
}

src/main/java/next/career/global/apiPayload/exception/GlobalErrorType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public enum GlobalErrorType implements ErrorType {
4141
ROAD_MAP_ACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "로드맵 할 일이 존재하지 않습니다"),
4242
ROAD_MAP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "로드맵이 존재하지 않습니다"),
4343
GET_RECOMMEND_ROADMAP_ACTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "로드맵 액션 추천에 실패했습니다"),
44+
GET_CERTIFICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "자격증 추천에 실패했습니다"),
4445

4546
MEMBER_OCCUPATION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "추천 직업이 존재하지 않습니다"),
4647

0 commit comments

Comments
 (0)