Skip to content

Commit 56a6517

Browse files
authored
Merge branch 'develop' into feat/#110
2 parents 480a21d + b5efe69 commit 56a6517

6 files changed

Lines changed: 163 additions & 2 deletions

File tree

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: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@ public ApiResponse<?> addRoadmapAction(
106106
return ApiResponse.success();
107107
}
108108

109-
110-
111109
@PutMapping("/{roadMapActionId}")
112110
@Operation(
113111
summary = "로드맵 액션 수정",
@@ -186,6 +184,47 @@ public ApiResponse<?> deleteRoadmap(
186184
) {
187185
roadmapService.deleteRoadmap(authDetails.getUser());
188186
return ApiResponse.success("로드맵이 성공적으로 삭제되었습니다.");
187+
@PostMapping("/certification")
188+
@Operation(
189+
summary = "사용자 맞춤형 AI 자격증 추천",
190+
description = """
191+
사용자의 직무 정보를 입력받아,
192+
해당 직무에 적합한 자격증 목록을 AI 모델을 통해 추천합니다.
193+
194+
예시 요청:
195+
{
196+
"occupation": "백엔드 개발자"
197+
}
198+
"""
199+
)
200+
public ApiResponse<RoadmapDto.CertificationResponse> getCertifications(
201+
@io.swagger.v3.oas.annotations.parameters.RequestBody(
202+
description = "자격증 추천 요청 DTO (occupation 필드 필수)",
203+
required = true
204+
)
205+
@RequestBody RoadmapDto.CertificationRequest request
206+
) {
207+
List<String> certifications = roadmapService.getCertifications(request.getOccupation());
208+
RoadmapDto.CertificationResponse response = RoadmapDto.CertificationResponse.of(certifications);
209+
return ApiResponse.success(response);
210+
}
211+
212+
@PatchMapping("/{roadmapId}/category")
213+
@Operation(
214+
summary = "로드맵 카테고리 수정",
215+
description = "특정 로드맵의 카테고리를 수정합니다."
216+
)
217+
public ApiResponse<?> updateRoadmap(
218+
@Parameter(
219+
description = "로드맵 ID",
220+
required = true,
221+
example = "1"
222+
)
223+
@PathVariable Long roadmapId,
224+
@RequestBody RoadmapDto.RoadMapCategoryUpdateRequest request
225+
) {
226+
roadmapService.updateRoadmapCategory(roadmapId, request.getCategory());
227+
return ApiResponse.success();
189228
}
190229

191230
}

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

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

src/main/java/next/career/domain/roadmap/entity/RoadMap.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@ public void updateCompleted() {
3939

4040
this.isCompleted = allCompleted;
4141
}
42+
43+
public void updateCategory(String category) {
44+
this.category = category;
45+
}
4246
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import next.career.domain.roadmap.entity.RoadmapInput;
1111
import next.career.domain.roadmap.repository.RoadMapRepository;
1212
import next.career.domain.roadmap.repository.RoadmapActionRepository;
13+
import next.career.domain.roadmap.repository.RoadmapInputRepository;
1314
import next.career.domain.user.entity.Member;
1415
import next.career.domain.user.repository.MemberRepository;
1516
import next.career.global.apiPayload.exception.CoreException;
@@ -28,6 +29,7 @@ public class RoadmapService {
2829
private final OpenAiService openAiService;
2930
private final RoadMapRepository roadMapRepository;
3031
private final RoadmapActionRepository roadmapActionRepository;
32+
private final RoadmapInputRepository roadmapInputRepository;
3133

3234
@Transactional
3335
public RecommendDto.RoadMapResponse recommendRoadMap(GetRoadMapDto.Request roadmapRequest, Member member) {
@@ -140,6 +142,19 @@ public void updateRoadmapInput(RoadmapDto.RoadMapUpdateRequest request, Member m
140142
RoadmapInput roadmapInput = member.getRoadmapInput();
141143

142144
roadmapInput.update(request.getCareer(), request.getPeriod(), request.getExperience());
145+
roadmapInputRepository.save(roadmapInput);
146+
}
147+
148+
public List<String> getCertifications(String occupation) {
149+
return openAiService.getCertificationList(occupation);
150+
}
151+
152+
@Transactional
153+
public void updateRoadmapCategory(Long roadmapId, String category) {
154+
RoadMap roadMap = roadMapRepository.findById(roadmapId)
155+
.orElseThrow(() -> new CoreException(GlobalErrorType.ROAD_MAP_NOT_FOUND));
156+
157+
roadMap.updateCategory(category);
143158
}
144159

145160
@Transactional

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)