-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
강의동 배치 추가 및
search_query
API에서 실 데이터가 내려가도록 작업 (#216)
- Loading branch information
1 parent
74ef245
commit f78b4ce
Showing
18 changed files
with
517 additions
and
13 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
batch/src/main/kotlin/lecturebuildings/SnuMapRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper | ||
import com.fasterxml.jackson.module.kotlin.readValue | ||
import com.wafflestudio.snu4t.lecturebuildings.api.SnuMapApi | ||
import com.wafflestudio.snu4t.lecturebuildings.data.SnuMapSearchResult | ||
import org.springframework.http.MediaType | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.reactive.function.client.awaitBody | ||
|
||
@Component | ||
class SnuMapRepository( | ||
private val snuMapApi: SnuMapApi, | ||
private val objectMapper: ObjectMapper, | ||
) { | ||
companion object { | ||
const val SNU_MAP_SEARCH_PATH = "/api/search.action" | ||
const val DEFAULT_SEARCH_PARAMS = "lang_type=KOR" | ||
} | ||
|
||
suspend fun getLectureBuildingSearchResult( | ||
buildingNum: String | ||
): SnuMapSearchResult = snuMapApi.get().uri { builder -> | ||
builder.path(SNU_MAP_SEARCH_PATH) | ||
.query(DEFAULT_SEARCH_PARAMS) | ||
.queryParam("search_word", buildingNum) | ||
.build() | ||
}.accept(MediaType.APPLICATION_JSON).retrieve().awaitBody<String>() | ||
.let { objectMapper.readValue<SnuMapSearchResult>(it) } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.api | ||
|
||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.web.reactive.function.client.ExchangeStrategies | ||
import org.springframework.web.reactive.function.client.WebClient | ||
|
||
@Configuration | ||
class SnuMapApiConfig() { | ||
companion object { | ||
const val SNU_MAP_BASE_URL = "https://map.snu.ac.kr" | ||
} | ||
|
||
@Bean | ||
fun snuMapSnuApi(): SnuMapApi { | ||
val exchangeStrategies: ExchangeStrategies = ExchangeStrategies.builder() | ||
.codecs { it.defaultCodecs().maxInMemorySize(-1) } // to unlimited memory size | ||
.build() | ||
|
||
return WebClient.builder().baseUrl(SNU_MAP_BASE_URL) | ||
.exchangeStrategies(exchangeStrategies) | ||
.defaultHeaders { | ||
it.setAll( | ||
mapOf( | ||
"User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36", | ||
"Referrer" to "https://map.snu.ac.kr/web/main.action" | ||
) | ||
) | ||
} | ||
.build().let(::SnuMapApi) | ||
} | ||
} | ||
|
||
class SnuMapApi(webClient: WebClient) : WebClient by webClient |
9 changes: 9 additions & 0 deletions
9
batch/src/main/kotlin/lecturebuildings/data/LectureBuildingUpdateResult.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.data | ||
|
||
import com.wafflestudio.snu4t.lectures.data.Lecture | ||
|
||
class LectureBuildingUpdateResult( | ||
val lecturesWithBuildingInfos: List<Lecture>, | ||
val lecturesWithOutBuildingInfos: List<Lecture>, | ||
val lecturesFailed: List<Lecture> | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.data | ||
|
||
class PlaceInfo( | ||
val rawString: String, | ||
val campus: Campus, | ||
val buildingNumber: String, | ||
) { | ||
companion object { | ||
fun getValuesOf(places: String): List<PlaceInfo> = places.split("/").map { PlaceInfo(it) }.filterNotNull() | ||
} | ||
} | ||
|
||
fun PlaceInfo(place: String): PlaceInfo? { | ||
val campus: Campus = when (place.first()) { | ||
'#' -> Campus.YEONGEON | ||
'*' -> Campus.PYEONGCHANG | ||
else -> Campus.GWANAK | ||
} | ||
|
||
val placeWithOutCampus = place.removePrefix("#").removePrefix("*") | ||
val splits = placeWithOutCampus.split("-").filter { !it.matches("^[A-Za-z]*$".toRegex()) } | ||
|
||
val buildingNumber = when (splits.count()) { | ||
3 -> if (splits[1].count() == 1) splits.dropLast(1).joinToString("-") else splits.first() | ||
else -> splits.firstOrNull() | ||
}?.let { | ||
it.trimStart { it == '0' } | ||
} ?: return null | ||
|
||
return PlaceInfo(place, campus, buildingNumber) | ||
} |
28 changes: 28 additions & 0 deletions
28
batch/src/main/kotlin/lecturebuildings/data/SnuMapSearchResult.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.data | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty | ||
|
||
data class SnuMapSearchResult( | ||
@JsonProperty("search_list") | ||
val searchList: List<SnuMapSearchItem>, | ||
) | ||
|
||
data class SnuMapSearchItem( | ||
@JsonProperty("lat_val") | ||
val latitudeInDMS: Double, | ||
@JsonProperty("lon_val") | ||
val longitudeInDMS: Double, | ||
@JsonProperty("lat_val1") | ||
val latitudeInDecimal: Double, | ||
@JsonProperty("lon_val1") | ||
val longitudeInDecimal: Double, | ||
@JsonProperty("vil_dong_nm") | ||
val buildingNumber: String? = null, | ||
val name: String, | ||
@JsonProperty("ename") | ||
val englishName: String? = null, | ||
@JsonProperty("con_type") | ||
val contentType: String, | ||
@JsonProperty("fac_type") | ||
val facType: String, | ||
) |
94 changes: 94 additions & 0 deletions
94
batch/src/main/kotlin/lecturebuildings/job/LectureBuildingPopulateJobConfig.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.job | ||
|
||
import com.wafflestudio.snu4t.common.enum.Semester | ||
import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuildingUpdateResult | ||
import com.wafflestudio.snu4t.lecturebuildings.service.LectureBuildingPopulateService | ||
import com.wafflestudio.snu4t.lectures.data.Lecture | ||
import com.wafflestudio.snu4t.lectures.repository.LectureRepository | ||
import kotlinx.coroutines.async | ||
import kotlinx.coroutines.awaitAll | ||
import kotlinx.coroutines.flow.toList | ||
import kotlinx.coroutines.runBlocking | ||
import org.slf4j.LoggerFactory | ||
import org.springframework.batch.core.Job | ||
import org.springframework.batch.core.Step | ||
import org.springframework.batch.core.configuration.annotation.JobScope | ||
import org.springframework.batch.core.job.builder.JobBuilder | ||
import org.springframework.batch.core.repository.JobRepository | ||
import org.springframework.batch.core.step.builder.StepBuilder | ||
import org.springframework.batch.repeat.RepeatStatus | ||
import org.springframework.beans.factory.annotation.Value | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.transaction.PlatformTransactionManager | ||
|
||
@Configuration | ||
class LectureBuildingPopulateJobConfig( | ||
private val lectureRepository: LectureRepository, | ||
private val lectureBuildingPopulateService: LectureBuildingPopulateService, | ||
) { | ||
private val log = LoggerFactory.getLogger(javaClass) | ||
|
||
private companion object { | ||
const val JOB_NAME = "lectureBuildingPopulateJob" | ||
const val STEP_NAME = "lectureBuildingPopulateStep" | ||
} | ||
|
||
@Bean | ||
fun lectureBuildingPopulateJob(jobRepository: JobRepository, lectureBuildingPopulateStep: Step): Job { | ||
return JobBuilder(JOB_NAME, jobRepository) | ||
.start(lectureBuildingPopulateStep) | ||
.build() | ||
} | ||
|
||
@Bean | ||
@JobScope | ||
fun lectureBuildingPopulateStep( | ||
jobRepository: JobRepository, | ||
transactionManager: PlatformTransactionManager, | ||
@Value("#{jobParameters[year]}") year: Int, | ||
@Value("#{jobParameters[semester]}") semester: Int, | ||
): Step = StepBuilder(STEP_NAME, jobRepository).tasklet( | ||
{ _, _ -> | ||
runBlocking { | ||
lectureRepository.findAllByYearAndSemester(year, Semester.getOfValue(semester)!!).toList() | ||
.let { tryToUpdateLectureBuildings(it) } | ||
.let { updateResult -> | ||
updateTimetableLectures(updateResult.lecturesWithBuildingInfos) | ||
} | ||
} | ||
RepeatStatus.FINISHED | ||
}, | ||
transactionManager | ||
).build() | ||
|
||
private suspend fun tryToUpdateLectureBuildings(lectures: List<Lecture>): LectureBuildingUpdateResult = runBlocking { | ||
val updateResult = lectureBuildingPopulateService.populateLectureBuildingsWithFetch(lectures) | ||
|
||
log.info( | ||
"강의 ${updateResult.lecturesWithBuildingInfos.count()}개의 강의동 정보를 업데이트 했습니다.\n${ | ||
updateResult.lecturesWithBuildingInfos | ||
.map { "${it.courseTitle}(${it.lectureNumber})" } | ||
.joinToString(", ") | ||
}" | ||
) | ||
log.info( | ||
"강의동 업데이트에 실패한 강의:\n ${ | ||
updateResult.lecturesFailed | ||
.map { "${it.courseTitle}(${it.lectureNumber}): ${it.classPlaceAndTimes.map { it.place }.joinToString(", ")}" } | ||
.joinToString("\n") | ||
}" | ||
) | ||
|
||
return@runBlocking updateResult | ||
} | ||
|
||
private suspend fun updateTimetableLectures(lectures: List<Lecture>) = runBlocking { | ||
lectures.map { | ||
async { | ||
val timetables = lectureBuildingPopulateService.populateLectureBuildingsOfTimetables(it) | ||
log.info("강의 ${it.courseTitle}(${it.courseNumber})}가 포함된 시간표 ${timetables.count()}개를 업데이트 했습니다.") | ||
} | ||
} | ||
}.awaitAll() | ||
} |
44 changes: 44 additions & 0 deletions
44
batch/src/main/kotlin/lecturebuildings/service/LectureBuildingFetchService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.wafflestudio.snu4t.lecturebuildings.service | ||
|
||
import com.wafflestudio.snu4t.lecturebuildings.SnuMapRepository | ||
import com.wafflestudio.snu4t.lecturebuildings.data.Campus | ||
import com.wafflestudio.snu4t.lecturebuildings.data.GeoCoordinate | ||
import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuilding | ||
import com.wafflestudio.snu4t.lecturebuildings.data.SnuMapSearchItem | ||
import com.wafflestudio.snu4t.lecturebuildings.data.SnuMapSearchResult | ||
import com.wafflestudio.snu4t.lecturebuildings.repository.LectureBuildingRepository | ||
import org.springframework.stereotype.Service | ||
|
||
interface LectureBuildingFetchService { | ||
suspend fun getSnuMapLectureBuilding(campus: Campus, buildingNumber: String): LectureBuilding? | ||
} | ||
@Service | ||
class LectureBuildingFetchServiceImpl( | ||
private val snuMapRepository: SnuMapRepository, | ||
private val lectureBuildingRepository: LectureBuildingRepository | ||
) : LectureBuildingFetchService { | ||
override suspend fun getSnuMapLectureBuilding( | ||
campus: Campus, | ||
buildingNumber: String | ||
): LectureBuilding? = lectureBuildingRepository.findByBuildingNumber(buildingNumber) | ||
?: selectMostProbableSearchItem(buildingNumber, snuMapRepository.getLectureBuildingSearchResult(buildingNumber)) | ||
?.let { | ||
lectureBuildingRepository.save( | ||
LectureBuilding( | ||
buildingNumber = it.buildingNumber!!, | ||
buildingNameKor = it.name, | ||
buildingNameEng = it.englishName, | ||
locationInDMS = GeoCoordinate(it.latitudeInDMS, it.longitudeInDMS), | ||
locationInDecimal = GeoCoordinate(it.latitudeInDecimal, it.longitudeInDecimal), | ||
campus = campus | ||
) | ||
) | ||
} | ||
|
||
private fun selectMostProbableSearchItem( | ||
buildingNumber: String, | ||
searchResult: SnuMapSearchResult | ||
): SnuMapSearchItem? = searchResult.searchList | ||
.filter { it.contentType == "F" && it.facType == "OTHER" && it.buildingNumber == buildingNumber } | ||
.minByOrNull { it.name.count() } | ||
} |
Oops, something went wrong.