Skip to content

Commit

Permalink
Merge pull request #151 from SSU-Plector/issue/143-matching-bot
Browse files Browse the repository at this point in the history
✨ [Feat] 유저의 입력 값 바탕 제일 높은 가중치의 개발자 3명 반환하는 API
  • Loading branch information
88dldl authored Jul 21, 2024
2 parents 360bbb0 + 2044dfe commit 97cc7f5
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 1 deletion.
13 changes: 13 additions & 0 deletions src/main/java/ssuPlector/controller/DeveloperController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -105,6 +106,18 @@ public ApiResponse<List<DeveloperSearchDTO>> searchDeveloper(
return ApiResponse.onSuccess("개발자 검색 완료.", developerList);
}

@Operation(
summary = "개발자 매칭 봇",
description = "유저의 입력 값(개발자 파트, 기술스택, 설명 등)을 바탕으로 제일 높은 가중치의 개발자 3명 반환합니다._숙희")
@GetMapping(value = "/match")
public ApiResponse<List<DeveloperSearchDTO>> matchDeveloper(
@Valid @ModelAttribute DeveloperMatchingDTO developerMatchingDTO,
@RequestParam(value = "developerInfo") String developerInfo) {
List<DeveloperSearchDTO> developerList =
developerService.matchDeveloper(developerInfo, developerMatchingDTO);
return ApiResponse.onSuccess("개발자 매칭 완료.", developerList);
}

@Operation(summary = "더미 개발자 프로필 수정", description = "더미 개발자 프로필을 수정합니다._숙희")
@PatchMapping("/dummy/update")
public ApiResponse<Long> updateDummyDeveloper(
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/ssuPlector/dto/request/DeveloperDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -128,4 +129,21 @@ public static class DummyDeveloperRequestDTO {
@MaxSizeThree private List<DevTools> devToolList;
@MaxSizeThree private List<TechStack> techStackList;
}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class DeveloperMatchingDTO {
@NotNull(message = "필수 입력값")
private Part part;

@NotNull(message = "필수 입력값")
private List<@NotNull DevLanguage> languageList;

private List<TechStack> techStackList;
private Boolean projectExperience;
private Long studentNumberMin;
private Long studentNumberMax;
}
}
Original file line number Diff line number Diff line change
@@ -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<Developer> findDevelopers(String sortType, Part part, Pageable pageable);

List<Developer> searchDeveloper(String developerName);

Map<Long, Double> matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -77,4 +83,64 @@ BooleanExpression part2Eq(Part part) {
public List<Developer> 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<Long, Double> matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO) {
// part, 개발 경험, 학번
List<Developer> 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<Long, Double> weight = new HashMap<>();

// 사용언어, 기술스택
for (Developer developer : developers) {
double tmpWeight = 0.0;

List<DevLanguage> devLanguageList = developer.getLanguageList();
List<DevLanguage> intersectionLang =
devLanguageList.stream()
.filter(requestDTO.getLanguageList()::contains)
.toList();

tmpWeight += intersectionLang.size() * 0.5;

List<TechStack> techStackList = developer.getTechStackList();
List<TechStack> intersectionTech =
techStackList.stream().filter(requestDTO.getTechStackList()::contains).toList();

tmpWeight += intersectionTech.size() * 0.3;

weight.put(developer.getId(), tmpWeight);
}
System.out.println(weight);
return weight;
}
}
1 change: 1 addition & 0 deletions src/main/java/ssuPlector/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ public interface DeveloperService {
Long createDummyDeveloper(DummyDeveloperRequestDTO requestDTO, MultipartFile image);

List<DeveloperSearchDTO> searchDeveloper(String developerName);

List<DeveloperSearchDTO> matchDeveloper(String developerInfo, DeveloperMatchingDTO requestDTO);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -137,4 +156,75 @@ public List<DeveloperSearchDTO> searchDeveloper(String developerName) {
List<Developer> developers = developerRepository.searchDeveloper(developerName);
return developers.stream().map(DeveloperConverter::toDeveloperSearchDTO).toList();
}

@Override
public List<DeveloperSearchDTO> 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<String> requestEntity = new HttpEntity<>(requestBody, headers);

ResponseEntity<String> response =
restTemplate.exchange(aiUrl, HttpMethod.POST, requestEntity, String.class);

// response
ObjectMapper objectMapper = new ObjectMapper();
List<Long> developerIds = new ArrayList<>();
try {
JsonNode developersNode = objectMapper.readTree(response.getBody()).path("developers");
Iterator<JsonNode> 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<Long, Double> 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<Pair<Long, Double>> sortedDeveloper =
weight.entrySet().stream()
.map(entry -> Pair.of(entry.getKey(), entry.getValue()))
.sorted((p1, p2) -> p2.getValue().compareTo(p1.getValue()))
.toList();

List<Developer> developerList =
sortedDeveloper.stream()
.limit(3)
.map(
m -> {
Optional<Developer> optionalDeveloper =
developerRepository.findById(m.getLeft());
return optionalDeveloper.orElseThrow(
() ->
new GlobalException(
GlobalErrorCode.DEVELOPER_NOT_FOUND));
})
.toList();
return developerList.stream().map(DeveloperConverter::toDeveloperSearchDTO).toList();
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}

0 comments on commit 97cc7f5

Please sign in to comment.