Skip to content

Commit 2c0b96c

Browse files
authored
Merge pull request #55 from GAT2025/feat/#54
[Feat/#54] AI 팀 추천 API 구현 && 기타 수정
2 parents 0963161 + c4af090 commit 2c0b96c

12 files changed

Lines changed: 167 additions & 29 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package gat_be.dev.common.config
2+
3+
import org.springframework.context.annotation.Bean
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.web.client.RestTemplate
6+
7+
@Configuration
8+
class AppConfig {
9+
@Bean
10+
fun restTemplate(): RestTemplate {
11+
return RestTemplate()
12+
}
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package gat_be.dev.common.config
2+
3+
import org.springframework.beans.factory.annotation.Value
4+
import org.springframework.context.annotation.Configuration
5+
6+
@Configuration
7+
class FastAPIConfig(
8+
@Value("\${fastapi.team-recoomend-url}")
9+
val teamRecommendUrl: String
10+
)

src/main/kotlin/gat_be/dev/domain/review/service/ReviewService.kt

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import gat_be.dev.common.exception.GeneralException
44
import gat_be.dev.common.status.ErrorStatus
55
import gat_be.dev.domain.review.dto.request.CreateReviewRequest
66
import gat_be.dev.domain.review.dto.response.ReviewInfo
7-
import gat_be.dev.domain.review.dto.response.ReviewListResponse
87
import gat_be.dev.domain.review.entity.Review
98
import gat_be.dev.domain.review.entity.ReviewCategory
109
import gat_be.dev.domain.review.enums.Category
1110
import gat_be.dev.domain.review.repository.ReviewCategoryRepository
1211
import gat_be.dev.domain.review.repository.ReviewRepository
13-
import gat_be.dev.domain.user.dto.response.UserReviewCategoryCount
12+
import gat_be.dev.domain.user.dto.response.UserReviewCategoryAverage
1413
import gat_be.dev.domain.user.dto.response.UserReviewDetails
1514
import gat_be.dev.domain.user.entity.User
1615
import jakarta.transaction.Transactional
@@ -54,24 +53,35 @@ class ReviewService(
5453

5554
// 유저의 리뷰 카테고리별 개수 집계
5655
fun getUserReviewDetails(user: User): UserReviewDetails {
56+
// 1. 사용자 리뷰 목록 조회
5757
val reviewList = reviewRepository.findAllWithCategoriesByTeamMemberId(user.userId)
5858

59-
val categoryCounts = reviewList
60-
.flatMap { it.categories }
61-
.groupingBy { it.category }
62-
.eachCount()
59+
// 2. 카테고리별 점수 리스트 구성
60+
val categoryScoreMap = mutableMapOf<Category, MutableList<Int>>()
6361

64-
val reviewCategoryCount = UserReviewCategoryCount(
65-
timePunctuality = categoryCounts[Category.TIME_PUNCTUALITY] ?: 0,
66-
communication = categoryCounts[Category.COMMUNICATION] ?: 0,
67-
skillAndContribution = categoryCounts[Category.SKILL_AND_CONTRIBUTION] ?: 0,
68-
collaborationAndTeamwork = categoryCounts[Category.COLLABORATION_AND_TEAMWORK] ?: 0
62+
reviewList.flatMap { it.categories }.forEach {
63+
val scores = categoryScoreMap.getOrPut(it.category) { mutableListOf() }
64+
scores.add(it.score)
65+
}
66+
67+
// 3. 카테고리별 평균 점수 계산
68+
val averageScores = categoryScoreMap.mapValues { (_, scores) ->
69+
if (scores.isNotEmpty()) scores.sum().toDouble() / scores.size else 0.0
70+
}
71+
72+
// 4. 평균 점수를 담은 DTO 생성
73+
val reviewCategoryAverage = UserReviewCategoryAverage(
74+
timePunctuality = averageScores[Category.TIME_PUNCTUALITY] ?: 0.0,
75+
communication = averageScores[Category.COMMUNICATION] ?: 0.0,
76+
skillAndContribution = averageScores[Category.SKILL_AND_CONTRIBUTION] ?: 0.0,
77+
collaborationAndTeamwork = averageScores[Category.COLLABORATION_AND_TEAMWORK] ?: 0.0
6978
)
7079

80+
// 5. 최종 결과 반환
7181
return UserReviewDetails(
7282
rating = user.rating,
7383
reviewCount = reviewList.size.toLong(),
74-
reviewCategoryCount = reviewCategoryCount,
84+
reviewCategoryAverage = reviewCategoryAverage
7585
)
7686
}
7787

src/main/kotlin/gat_be/dev/domain/team/controller/TeamController.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package gat_be.dev.domain.team.controller
33
import ApiResponse
44
import gat_be.dev.common.status.SuccessStatus
55
import gat_be.dev.domain.team.dto.request.CreateTeamRequest
6+
import gat_be.dev.domain.team.dto.request.TeamRecommendRequest
67
import gat_be.dev.domain.team.dto.request.UpdateTeamRequest
78
import gat_be.dev.domain.team.dto.response.*
89
import gat_be.dev.domain.team.enums.TechStackRole
910
import gat_be.dev.domain.team.facade.TeamListFacade
1011
import gat_be.dev.domain.team.facade.TeamInfoFacade
1112
import gat_be.dev.domain.team.facade.TeamManageFacade
13+
import gat_be.dev.domain.team.infra.service.TeamAIService
1214
import io.swagger.v3.oas.annotations.Operation
1315
import io.swagger.v3.oas.annotations.media.Content
1416
import io.swagger.v3.oas.annotations.media.Schema
@@ -25,6 +27,7 @@ class TeamController(
2527
private val teamManageFacade: TeamManageFacade,
2628
private val teamInfoFacade: TeamInfoFacade,
2729
private val teamListFacade: TeamListFacade,
30+
private val teamAIService: TeamAIService
2831
) {
2932

3033
@PostMapping("")
@@ -95,4 +98,15 @@ class TeamController(
9598
return ApiResponse.success(SuccessStatus.GET_TEAM_LIST_FOR_SEARCH_SUCCESS, response)
9699
}
97100

101+
@Operation(summary = "유저가 선택한 카테고리 기반 AI 팀 추천")
102+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "유저가 선택한 카테고리 기반 AI 팀 추천", content = [Content(mediaType = "application/json", schema = Schema(implementation = TeamListResponse::class))])
103+
@PostMapping("/recommend")
104+
fun recommendTeamListByAI(
105+
@AuthenticationPrincipal userId: Long,
106+
@Valid @RequestBody request: TeamRecommendRequest
107+
): ResponseEntity<ApiResponse<TeamListResponse>> {
108+
val response = teamAIService.recommendTeamListByAI(userId,request)
109+
return ApiResponse.success(SuccessStatus.GET_TEAM_LIST_FOR_SEARCH_SUCCESS, response)
110+
}
111+
98112
}

src/main/kotlin/gat_be/dev/domain/team/dto/request/TeamRequest.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ data class CreateTeamRequest(
4646
val portfolioLink2: String?,
4747

4848
@field:Size(max = 30, message = "팀장의 연락처 정보는 30자를 초과할 수 없습니다.")
49-
val contactInfo: String?
49+
val contactInfo: String?,
50+
51+
@field:NotBlank(message = "팀의 인재상은 필수입니다.")
52+
val idealTraits: String
5053
)
5154

5255
data class UpdateTeamRequest(
@@ -78,4 +81,10 @@ data class UpdateTeamRequest(
7881

7982
@field:Size(max = 30, message = "팀장의 연락처 정보는 30자를 초과할 수 없습니다.")
8083
val contactInfo: String?
81-
)
84+
)
85+
86+
data class TeamRecommendRequest(
87+
val skills: List<String>,
88+
val interests: List<String>,
89+
val teamPreference: String?
90+
)

src/main/kotlin/gat_be/dev/domain/team/dto/response/TeamResponse.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ data class TeamInfoResponse(
2323
val projectStartDate: String,
2424
val projectEndDate: String,
2525
val description: String,
26+
val idealTraits: String,
2627
val teamLeaderInfo: TeamLeaderInfo
2728
) {
2829
companion object {
@@ -39,6 +40,7 @@ data class TeamInfoResponse(
3940
projectStartDate = team.formattedProjectStartDate,
4041
projectEndDate = team.formattedProjectEndDate,
4142
description = team.description,
43+
idealTraits = team.idealTraits,
4244
teamLeaderInfo = TeamLeaderInfo.from(leaderName, team.leader)
4345
)
4446
}

src/main/kotlin/gat_be/dev/domain/team/entity/Team.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Team(
1616
description: String,
1717
applicationDueDate: LocalDate,
1818
projectStartDate: LocalDate,
19-
projectEndDate: LocalDate
19+
projectEndDate: LocalDate,
20+
idealTraits:String
2021
): BaseEntity() {
2122

2223
// 1. Properties
@@ -35,6 +36,10 @@ class Team(
3536
var description: String = description
3637
protected set
3738

39+
@Column(nullable = false, length = 255)
40+
var idealTraits: String = idealTraits
41+
protected set
42+
3843
@Column(nullable = false)
3944
var applicationDueDate: LocalDate = applicationDueDate
4045
protected set
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package gat_be.dev.domain.team.infra.dto
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty
4+
5+
data class AITeamRecommendRequest(
6+
@JsonProperty("user_id")
7+
val userId: Long,
8+
val skills: List<String>,
9+
val interests: List<String>,
10+
@JsonProperty("team_preference")
11+
val teamPreference: String?
12+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package gat_be.dev.domain.team.infra.dto
2+
3+
import gat_be.dev.domain.team.dto.response.TeamInfoWithIsScrapped
4+
5+
data class TeamListResponse(
6+
val isLast: Boolean,
7+
val teamList: List<TeamInfoWithIsScrapped>
8+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package gat_be.dev.domain.team.infra.service
2+
3+
import gat_be.dev.common.config.FastAPIConfig
4+
import gat_be.dev.common.exception.GeneralException
5+
import gat_be.dev.common.status.ErrorStatus
6+
import gat_be.dev.domain.team.dto.request.TeamRecommendRequest
7+
import gat_be.dev.domain.team.infra.dto.AITeamRecommendRequest
8+
import gat_be.dev.domain.team.dto.response.TeamListResponse
9+
import org.springframework.core.ParameterizedTypeReference
10+
import org.springframework.http.*
11+
import org.springframework.stereotype.Component
12+
import org.springframework.web.client.HttpClientErrorException
13+
import org.springframework.web.client.RestTemplate
14+
15+
@Component
16+
class TeamAIService(
17+
private val restTemplate: RestTemplate,
18+
private val fastAPIConfig: FastAPIConfig
19+
) {
20+
fun recommendTeamListByAI(
21+
userId:Long,
22+
request: TeamRecommendRequest
23+
): TeamListResponse {
24+
25+
val aiTeamRecommendRequest = AITeamRecommendRequest(
26+
userId = userId,
27+
skills = request.skills,
28+
interests = request.interests,
29+
teamPreference = request.teamPreference,
30+
)
31+
32+
val headers = HttpHeaders().apply {
33+
contentType = MediaType.APPLICATION_JSON
34+
}
35+
36+
val entity = HttpEntity(aiTeamRecommendRequest, headers)
37+
38+
val response = try {
39+
restTemplate.exchange(
40+
fastAPIConfig.teamRecommendUrl,
41+
HttpMethod.POST,
42+
entity,
43+
object : ParameterizedTypeReference<TeamListResponse>() {}
44+
)
45+
} catch (e: HttpClientErrorException) {
46+
throw RuntimeException("FastAPI 요청 실패: ${e.responseBodyAsString}", e)
47+
} catch (e: Exception) {
48+
throw RuntimeException("FastAPI 호출 중 오류 발생", e)
49+
}
50+
51+
return response.body ?: throw GeneralException(ErrorStatus.INTERNAL_SERVER_ERROR)
52+
}
53+
54+
}
55+

0 commit comments

Comments
 (0)