diff --git a/src/main/java/ssuPlector/controller/DeveloperController.java b/src/main/java/ssuPlector/controller/DeveloperController.java index cd51b24..3ec2f64 100644 --- a/src/main/java/ssuPlector/controller/DeveloperController.java +++ b/src/main/java/ssuPlector/controller/DeveloperController.java @@ -23,6 +23,7 @@ import ssuPlector.domain.Developer; import ssuPlector.dto.request.DeveloperDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperListRequestDTO; +import ssuPlector.dto.request.DeveloperDTO.DeveloperMatchingDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperRequestDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperUpdateRequestDTO; import ssuPlector.dto.response.DeveloperDTO.DeveloperDetailDTO; @@ -105,6 +106,18 @@ public ApiResponse> searchDeveloper( return ApiResponse.onSuccess("개발자 검색 완료.", developerList); } + @Operation( + summary = "개발자 매칭 봇", + description = "유저의 입력 값(개발자 파트, 기술스택, 설명 등)을 바탕으로 제일 높은 가중치의 개발자 3명 반환합니다._숙희") + @GetMapping(value = "/match") + public ApiResponse> matchDeveloper( + @Valid @ModelAttribute DeveloperMatchingDTO developerMatchingDTO, + @RequestParam(value = "developerInfo") String developerInfo) { + List developerList = + developerService.matchDeveloper(developerInfo, developerMatchingDTO); + return ApiResponse.onSuccess("개발자 매칭 완료.", developerList); + } + @Operation(summary = "더미 개발자 프로필 수정", description = "더미 개발자 프로필을 수정합니다._숙희") @PatchMapping("/dummy/update") public ApiResponse updateDummyDeveloper( diff --git a/src/main/java/ssuPlector/dto/request/DeveloperDTO.java b/src/main/java/ssuPlector/dto/request/DeveloperDTO.java index f65b2f4..b931067 100644 --- a/src/main/java/ssuPlector/dto/request/DeveloperDTO.java +++ b/src/main/java/ssuPlector/dto/request/DeveloperDTO.java @@ -3,6 +3,7 @@ import java.util.List; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -128,4 +129,21 @@ public static class DummyDeveloperRequestDTO { @MaxSizeThree private List devToolList; @MaxSizeThree private List techStackList; } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class DeveloperMatchingDTO { + @NotNull(message = "필수 입력값") + private Part part; + + @NotNull(message = "필수 입력값") + private List<@NotNull DevLanguage> languageList; + + private List techStackList; + private Boolean projectExperience; + private Long studentNumberMin; + private Long studentNumberMax; + } } diff --git a/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryCustom.java b/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryCustom.java index feae87a..f515614 100644 --- a/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryCustom.java +++ b/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryCustom.java @@ -1,16 +1,20 @@ package ssuPlector.repository.developer; import java.util.List; +import java.util.Map; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import ssuPlector.domain.Developer; import ssuPlector.domain.category.Part; +import ssuPlector.dto.request.DeveloperDTO.DeveloperMatchingDTO; public interface DeveloperRepositoryCustom { Page findDevelopers(String sortType, Part part, Pageable pageable); List searchDeveloper(String developerName); + + Map matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO); } diff --git a/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryImpl.java b/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryImpl.java index 31e0756..a52a825 100644 --- a/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryImpl.java +++ b/src/main/java/ssuPlector/repository/developer/DeveloperRepositoryImpl.java @@ -1,9 +1,12 @@ package ssuPlector.repository.developer; import static ssuPlector.domain.QDeveloper.developer; +import static ssuPlector.domain.QProjectDeveloper.projectDeveloper; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -18,7 +21,10 @@ import lombok.RequiredArgsConstructor; import ssuPlector.domain.Developer; +import ssuPlector.domain.category.DevLanguage; import ssuPlector.domain.category.Part; +import ssuPlector.domain.category.TechStack; +import ssuPlector.dto.request.DeveloperDTO.DeveloperMatchingDTO; import ssuPlector.global.exception.GlobalException; import ssuPlector.global.response.code.GlobalErrorCode; @@ -77,4 +83,64 @@ BooleanExpression part2Eq(Part part) { public List searchDeveloper(String name) { return queryFactory.selectFrom(developer).where(developer.name.contains(name)).fetch(); } + + BooleanExpression searchDeveloperStudentNumber(Long min, Long max) { + if (min == null) min = 0L; + if (max == null) max = 100L; + return developer.studentNumber.between(min.toString(), max.toString()); + } + + BooleanExpression searchDeveloperProjectExperience(Boolean experience) { + if (experience == null || !experience) { + return developer.isNotNull(); + } + return queryFactory + .selectFrom(projectDeveloper) + .where(projectDeveloper.developer.eq(developer)) + .exists(); + } + + @Override + public Map matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO) { + // part, 개발 경험, 학번 + List developers = + queryFactory + .selectFrom(developer) + .where( + (part1Eq(requestDTO.getPart()).or(part2Eq(requestDTO.getPart()))) + .and( + searchDeveloperStudentNumber( + requestDTO.getStudentNumberMin(), + requestDTO.getStudentNumberMax())) + .and( + searchDeveloperProjectExperience( + requestDTO.getProjectExperience()))) + .fetch(); + if (developers.size() == 0) throw new GlobalException(GlobalErrorCode.DEVELOPER_NOT_FOUND); + + Map weight = new HashMap<>(); + + // 사용언어, 기술스택 + for (Developer developer : developers) { + double tmpWeight = 0.0; + + List devLanguageList = developer.getLanguageList(); + List intersectionLang = + devLanguageList.stream() + .filter(requestDTO.getLanguageList()::contains) + .toList(); + + tmpWeight += intersectionLang.size() * 0.5; + + List techStackList = developer.getTechStackList(); + List intersectionTech = + techStackList.stream().filter(requestDTO.getTechStackList()::contains).toList(); + + tmpWeight += intersectionTech.size() * 0.3; + + weight.put(developer.getId(), tmpWeight); + } + System.out.println(weight); + return weight; + } } diff --git a/src/main/java/ssuPlector/security/SecurityConfig.java b/src/main/java/ssuPlector/security/SecurityConfig.java index b70b2c0..02d79a3 100644 --- a/src/main/java/ssuPlector/security/SecurityConfig.java +++ b/src/main/java/ssuPlector/security/SecurityConfig.java @@ -71,6 +71,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/developers/dummy", "/api/developers/dummy/update", "/api/developers/search", + "/api/developers/match", "/api/assistant/pm/summary", "/api/assistant/pm/meeting", "/api/assistant/designer/branding", diff --git a/src/main/java/ssuPlector/service/developer/DeveloperService.java b/src/main/java/ssuPlector/service/developer/DeveloperService.java index 9bd411f..6fe2cc0 100644 --- a/src/main/java/ssuPlector/service/developer/DeveloperService.java +++ b/src/main/java/ssuPlector/service/developer/DeveloperService.java @@ -28,4 +28,6 @@ public interface DeveloperService { Long createDummyDeveloper(DummyDeveloperRequestDTO requestDTO, MultipartFile image); List searchDeveloper(String developerName); + + List matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO); } diff --git a/src/main/java/ssuPlector/service/developer/DeveloperServiceImpl.java b/src/main/java/ssuPlector/service/developer/DeveloperServiceImpl.java index ccb0f42..7b806e5 100644 --- a/src/main/java/ssuPlector/service/developer/DeveloperServiceImpl.java +++ b/src/main/java/ssuPlector/service/developer/DeveloperServiceImpl.java @@ -1,16 +1,31 @@ package ssuPlector.service.developer; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.UUID; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import lombok.RequiredArgsConstructor; import ssuPlector.aws.s3.AmazonS3Manager; import ssuPlector.converter.DeveloperConverter; @@ -23,6 +38,7 @@ import ssuPlector.domain.category.TechStack; import ssuPlector.dto.request.DeveloperDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperListRequestDTO; +import ssuPlector.dto.request.DeveloperDTO.DeveloperMatchingDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperRequestDTO; import ssuPlector.dto.request.DeveloperDTO.DeveloperUpdateRequestDTO; import ssuPlector.dto.response.DeveloperDTO.DeveloperSearchDTO; @@ -42,6 +58,9 @@ public class DeveloperServiceImpl implements DeveloperService { private final AmazonS3Manager s3Manager; private final UuidRepository uuidRepository; + @Value("${sp.ai.url}") + private String aiUrl; + @Override @Transactional public Long createDeveloper(String email, DeveloperRequestDTO requestDTO) { @@ -137,4 +156,75 @@ public List searchDeveloper(String developerName) { List developers = developerRepository.searchDeveloper(developerName); return developers.stream().map(DeveloperConverter::toDeveloperSearchDTO).toList(); } + + @Override + public List matchDeveloper( + String developerInfo, DeveloperMatchingDTO requestDTO) { + // request + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + + headers.setContentType(MediaType.APPLICATION_JSON); + String requestBody = + String.format( + "{\"part\":\"%s\", \"request\":\"%s\"}", + requestDTO.getPart(), developerInfo); + + HttpEntity requestEntity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response = + restTemplate.exchange(aiUrl, HttpMethod.POST, requestEntity, String.class); + + // response + ObjectMapper objectMapper = new ObjectMapper(); + List developerIds = new ArrayList<>(); + try { + JsonNode developersNode = objectMapper.readTree(response.getBody()).path("developers"); + Iterator elements = developersNode.elements(); + while (elements.hasNext()) { + Long developerId = elements.next().path("developer_id").asLong(); + developerIds.add(developerId); + } + } catch (JsonProcessingException e) { + throw new GlobalException(GlobalErrorCode.INVALID_REQUEST_INFO); + } + + Map weight = developerRepository.matchDeveloper(developerInfo, requestDTO); + + double w = 0.5; + for (Long developerId : developerIds) { + Developer developer = + developerRepository + .findById(developerId) + .orElseThrow( + () -> new GlobalException(GlobalErrorCode.DEVELOPER_NOT_FOUND)); + + weight.putIfAbsent(developer.getId(), 0.0); + + weight.put(developer.getId(), weight.get(developer.getId()) + w); + w -= 0.1; + } + + // sort + List> sortedDeveloper = + weight.entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), entry.getValue())) + .sorted((p1, p2) -> p2.getValue().compareTo(p1.getValue())) + .toList(); + + List developerList = + sortedDeveloper.stream() + .limit(3) + .map( + m -> { + Optional optionalDeveloper = + developerRepository.findById(m.getLeft()); + return optionalDeveloper.orElseThrow( + () -> + new GlobalException( + GlobalErrorCode.DEVELOPER_NOT_FOUND)); + }) + .toList(); + return developerList.stream().map(DeveloperConverter::toDeveloperSearchDTO).toList(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a4ac232..e7ab725 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -69,6 +69,9 @@ openai: api: key: ${OPENAI_API_KEY} url: "https://api.openai.com/v1/chat/completions" + image-url: "https://api.openai.com/v1/images/generations" +ai: + url: ${SP_AI_URL} --- # DEV spring: @@ -129,4 +132,5 @@ openai: key: ${OPENAI_API_KEY} url: "https://api.openai.com/v1/chat/completions" image-url: "https://api.openai.com/v1/images/generations" - +ai: + url: ${SP_DEV_AI_URL}