Skip to content

Commit

Permalink
ev-service 마이그레이션 (#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
asp345 authored Jan 9, 2025
1 parent cb78fad commit 5b6db29
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 1 deletion.
7 changes: 6 additions & 1 deletion api/src/main/kotlin/filter/ErrorWebFilter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.wafflestudio.snu4t.filter

import com.fasterxml.jackson.databind.ObjectMapper
import com.wafflestudio.snu4t.common.exception.ErrorType
import com.wafflestudio.snu4t.common.exception.EvServiceProxyException
import com.wafflestudio.snu4t.common.exception.Snu4tException
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
Expand All @@ -26,9 +27,13 @@ class ErrorWebFilter(
): Mono<Void> {
return chain.filter(exchange)
.onErrorResume { throwable ->
val errorBody: ErrorBody
val errorBody: Any
val httpStatusCode: HttpStatusCode
when (throwable) {
is EvServiceProxyException -> {
httpStatusCode = throwable.statusCode
errorBody = throwable.errorBody
}
is Snu4tException -> {
httpStatusCode = throwable.error.httpStatus
errorBody = makeErrorBody(throwable)
Expand Down
44 changes: 44 additions & 0 deletions api/src/main/kotlin/handler/EvServiceHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.wafflestudio.snu4t.handler

import com.wafflestudio.snu4t.evaluation.service.EvService
import com.wafflestudio.snu4t.middleware.SnuttRestApiDefaultMiddleware
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.bodyToMono

@Component
class EvServiceHandler(
private val evService: EvService,
snuttRestApiDefaultMiddleware: SnuttRestApiDefaultMiddleware,
) : ServiceHandler(snuttRestApiDefaultMiddleware) {
suspend fun handleGet(req: ServerRequest) =
handle(req) {
val body = req.bodyToMono<String>().awaitSingleOrNull() ?: ""
evService.handleRouting(req.userId, req.pathVariable("requestPath"), req.queryParams(), body, HttpMethod.GET)
}

suspend fun handlePost(req: ServerRequest) =
handle(req) {
val body = req.bodyToMono<String>().awaitSingleOrNull() ?: ""
evService.handleRouting(req.userId, req.pathVariable("requestPath"), req.queryParams(), body, HttpMethod.POST)
}

suspend fun handleDelete(req: ServerRequest) =
handle(req) {
val body = req.bodyToMono<String>().awaitSingleOrNull() ?: ""
evService.handleRouting(req.userId, req.pathVariable("requestPath"), req.queryParams(), body, HttpMethod.DELETE)
}

suspend fun handlePatch(req: ServerRequest) =
handle(req) {
val body = req.bodyToMono<String>().awaitSingleOrNull() ?: ""
evService.handleRouting(req.userId, req.pathVariable("requestPath"), req.queryParams(), body, HttpMethod.PATCH)
}

suspend fun getMyLatestLectures(req: ServerRequest) =
handle(req) {
evService.getMyLatestLectures(req.userId, req.queryParams())
}
}
12 changes: 12 additions & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.wafflestudio.snu4t.handler.ConfigHandler
import com.wafflestudio.snu4t.handler.CoursebookHandler
import com.wafflestudio.snu4t.handler.DeviceHandler
import com.wafflestudio.snu4t.handler.EvHandler
import com.wafflestudio.snu4t.handler.EvServiceHandler
import com.wafflestudio.snu4t.handler.FeedbackHandler
import com.wafflestudio.snu4t.handler.FriendHandler
import com.wafflestudio.snu4t.handler.FriendTableHandler
Expand Down Expand Up @@ -68,6 +69,7 @@ class MainRouter(
private val tagHandler: TagHandler,
private val feedbackHandler: FeedbackHandler,
private val staticPageHandler: StaticPageHandler,
private val evServiceHandler: EvServiceHandler,
) {
@Bean
fun healthCheck() =
Expand Down Expand Up @@ -298,6 +300,16 @@ class MainRouter(
GET("/ev/lectures/{lectureId}/summary", evHandler::getLectureEvaluationSummary)
}

@Bean
fun evServiceRouter() =
v1CoRouter {
GET("/ev-service/v1/users/me/lectures/latest", evServiceHandler::getMyLatestLectures)
GET("/ev-service/{*requestPath}", evServiceHandler::handleGet)
POST("/ev-service/{*requestPath}", evServiceHandler::handlePost)
DELETE("/ev-service/{*requestPath}", evServiceHandler::handleDelete)
PATCH("/ev-service/{*requestPath}", evServiceHandler::handlePatch)
}

@Bean
@CoursebookDocs
fun coursebookRouter() =
Expand Down
2 changes: 2 additions & 0 deletions api/src/test/kotlin/timetable/TimetableIntegTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.wafflestudio.snu4t.timetable
import BaseIntegTest
import com.ninjasquad.springmockk.MockkBean
import com.wafflestudio.snu4t.evaluation.repository.SnuttEvRepository
import com.wafflestudio.snu4t.evaluation.service.EvService
import com.wafflestudio.snu4t.fixture.TimetableFixture
import com.wafflestudio.snu4t.fixture.UserFixture
import com.wafflestudio.snu4t.handler.RequestContext
Expand All @@ -25,6 +26,7 @@ import timetables.dto.TimetableBriefDto
class TimetableIntegTest(
@MockkBean private val mockMiddleware: SnuttRestApiDefaultMiddleware,
@MockkBean private val mockSnuttEvRepository: SnuttEvRepository,
@MockkBean private val evService: EvService,
val mainRouter: MainRouter,
val timetableFixture: TimetableFixture,
val userFixture: UserFixture,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.wafflestudio.snu4t.common.exception

import org.springframework.http.HttpStatusCode

class EvServiceProxyException(
val statusCode: HttpStatusCode,
val errorBody: Map<String, Any?>,
) : RuntimeException()
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface CoursebookRepository : CoroutineCrudRepository<Coursebook, String> {
suspend fun findFirstByOrderByYearDescSemesterDesc(): Coursebook

suspend fun findAllByOrderByYearDescSemesterDesc(): List<Coursebook>

suspend fun findTop3ByOrderByYearDescSemesterDesc(): List<Coursebook>
}
7 changes: 7 additions & 0 deletions core/src/main/kotlin/coursebook/service/CoursebookService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ interface CoursebookService {
suspend fun getLatestCoursebook(): Coursebook

suspend fun getCoursebooks(): List<Coursebook>

suspend fun getLastTwoCourseBooksBeforeCurrent(): List<Coursebook>
}

@Service
class CoursebookServiceImpl(private val coursebookRepository: CoursebookRepository) : CoursebookService {
override suspend fun getLatestCoursebook(): Coursebook = coursebookRepository.findFirstByOrderByYearDescSemesterDesc()

override suspend fun getCoursebooks(): List<Coursebook> = coursebookRepository.findAllByOrderByYearDescSemesterDesc()

override suspend fun getLastTwoCourseBooksBeforeCurrent(): List<Coursebook> =
coursebookRepository.findTop3ByOrderByYearDescSemesterDesc().slice(
1..2,
)
}
25 changes: 25 additions & 0 deletions core/src/main/kotlin/evaluation/dto/EvLectureInfoDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.wafflestudio.snu4t.evaluation.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.wafflestudio.snu4t.common.enum.Semester
import com.wafflestudio.snu4t.timetables.data.TimetableLecture

data class EvLectureInfoDto(
val year: Int,
val semester: Int,
val instructor: String?,
@JsonProperty("course_number")
val courseNumber: String?,
)

fun EvLectureInfoDto(
timetableLecture: TimetableLecture,
year: Int,
semester: Semester,
): EvLectureInfoDto =
EvLectureInfoDto(
year = year,
semester = semester.value,
instructor = timetableLecture.instructor,
courseNumber = timetableLecture.courseNumber,
)
18 changes: 18 additions & 0 deletions core/src/main/kotlin/evaluation/dto/EvUserDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.wafflestudio.snu4t.evaluation.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.wafflestudio.snu4t.users.data.User

data class EvUserDto(
val id: String?,
val email: String?,
@JsonProperty("local_id")
val localId: String?,
)

fun EvUserDto(user: User) =
EvUserDto(
id = user.id,
email = user.email,
localId = user.credential.localId,
)
117 changes: 117 additions & 0 deletions core/src/main/kotlin/evaluation/service/EvService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.wafflestudio.snu4t.evaluation.service

import com.fasterxml.jackson.databind.ObjectMapper
import com.wafflestudio.snu4t.common.exception.EvServiceProxyException
import com.wafflestudio.snu4t.common.util.buildMultiValueMap
import com.wafflestudio.snu4t.config.SnuttEvWebClient
import com.wafflestudio.snu4t.coursebook.service.CoursebookService
import com.wafflestudio.snu4t.evaluation.dto.EvLectureInfoDto
import com.wafflestudio.snu4t.evaluation.dto.EvUserDto
import com.wafflestudio.snu4t.timetables.service.TimetableService
import com.wafflestudio.snu4t.users.service.UserService
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatusCode
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.util.MultiValueMap
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.bodyToMono
import reactor.core.publisher.Mono

@Service
class EvService(
private val snuttEvWebClient: SnuttEvWebClient,
private val timetableService: TimetableService,
private val coursebookService: CoursebookService,
private val userService: UserService,
private val objectMapper: ObjectMapper,
) {
suspend fun handleRouting(
userId: String,
requestPath: String,
requestQueryParams: MultiValueMap<String, String> = buildMultiValueMap(mapOf()),
originalBody: String,
method: HttpMethod,
): Map<String, Any?> {
val result: MutableMap<String, Any?> =
snuttEvWebClient.method(method)
.uri { builder -> builder.path(requestPath).queryParams(requestQueryParams).build() }
.header("Snutt-User-Id", userId)
.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(BodyInserters.fromValue(originalBody))
.retrieve()
.onStatus(HttpStatusCode::isError) { response ->
response.bodyToMono<Map<String, Any?>>()
.flatMap { errorBody ->
Mono.error(EvServiceProxyException(response.statusCode(), errorBody))
}
}
.bodyToMono<MutableMap<String, Any?>>()
.awaitSingle()
return updateUserInfo(result)
}

suspend fun getMyLatestLectures(
userId: String,
requestQueryParams: MultiValueMap<String, String> = buildMultiValueMap(mapOf()),
): Map<String, Any?> {
val recentLectures: List<EvLectureInfoDto> =
coursebookService.getLastTwoCourseBooksBeforeCurrent().flatMap { coursebook ->
timetableService.getTimetablesBySemester(userId, coursebook.year, coursebook.semester)
.toList()
.flatMap { timetable ->
timetable.lectures.map { lecture ->
EvLectureInfoDto(
lecture,
coursebook.year,
coursebook.semester,
)
}
}
}

val lectureInfoParam = objectMapper.writeValueAsString(recentLectures)
return snuttEvWebClient.get()
.uri { builder ->
builder
.path("/v1/users/me/lectures/latest")
.queryParam("snutt_lecture_info", "{lectureInfoParam}")
.queryParams(requestQueryParams)
.build(lectureInfoParam)
}
.header("Snutt-User-Id", userId)
.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.onStatus(HttpStatusCode::isError) { response ->
response.bodyToMono<Map<String, Any?>>()
.flatMap { errorBody ->
Mono.error(EvServiceProxyException(response.statusCode(), errorBody))
}
}
.bodyToMono<MutableMap<String, Any?>>()
.awaitSingle()
}

@Suppress("UNCHECKED_CAST")
private suspend fun updateUserInfo(data: MutableMap<String, Any?>): MutableMap<String, Any?> {
val updatedMap: MutableMap<String, Any?> = mutableMapOf()
for ((k, v) in data.entries) {
if (k == "user_id") {
val userDto = runCatching { EvUserDto(userService.getUser(v as String)) }.getOrNull()
updatedMap["user"] = userDto
} else {
when (v) {
is List<*> -> updatedMap[k] = v.map { updateUserInfo(it as MutableMap<String, Any?>) }
is MutableMap<*, *> -> updatedMap[k] = updateUserInfo(v as MutableMap<String, Any?>)
else -> updatedMap[k] = v
}
}
}
return updatedMap
}
}

0 comments on commit 5b6db29

Please sign in to comment.