diff --git a/batch/src/main/kotlin/lecturebuildings/SnuMapRepository.kt b/batch/src/main/kotlin/lecturebuildings/SnuMapRepository.kt new file mode 100644 index 00000000..a64decf7 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/SnuMapRepository.kt @@ -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() + .let { objectMapper.readValue(it) } +} diff --git a/batch/src/main/kotlin/lecturebuildings/api/SnuMapApi.kt b/batch/src/main/kotlin/lecturebuildings/api/SnuMapApi.kt new file mode 100644 index 00000000..151e6c68 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/api/SnuMapApi.kt @@ -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 diff --git a/batch/src/main/kotlin/lecturebuildings/data/LectureBuildingUpdateResult.kt b/batch/src/main/kotlin/lecturebuildings/data/LectureBuildingUpdateResult.kt new file mode 100644 index 00000000..f3b59859 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/data/LectureBuildingUpdateResult.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.snu4t.lecturebuildings.data + +import com.wafflestudio.snu4t.lectures.data.Lecture + +class LectureBuildingUpdateResult( + val lecturesWithBuildingInfos: List, + val lecturesWithOutBuildingInfos: List, + val lecturesFailed: List +) diff --git a/batch/src/main/kotlin/lecturebuildings/data/PlaceInfo.kt b/batch/src/main/kotlin/lecturebuildings/data/PlaceInfo.kt new file mode 100644 index 00000000..6eaf9d06 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/data/PlaceInfo.kt @@ -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 = 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) +} diff --git a/batch/src/main/kotlin/lecturebuildings/data/SnuMapSearchResult.kt b/batch/src/main/kotlin/lecturebuildings/data/SnuMapSearchResult.kt new file mode 100644 index 00000000..7ec9e568 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/data/SnuMapSearchResult.kt @@ -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, +) + +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, +) diff --git a/batch/src/main/kotlin/lecturebuildings/job/LectureBuildingPopulateJobConfig.kt b/batch/src/main/kotlin/lecturebuildings/job/LectureBuildingPopulateJobConfig.kt new file mode 100644 index 00000000..54f71b92 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/job/LectureBuildingPopulateJobConfig.kt @@ -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): 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) = runBlocking { + lectures.map { + async { + val timetables = lectureBuildingPopulateService.populateLectureBuildingsOfTimetables(it) + log.info("강의 ${it.courseTitle}(${it.courseNumber})}가 포함된 시간표 ${timetables.count()}개를 업데이트 했습니다.") + } + } + }.awaitAll() +} diff --git a/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingFetchService.kt b/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingFetchService.kt new file mode 100644 index 00000000..a625d443 --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingFetchService.kt @@ -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() } +} diff --git a/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingPopulateService.kt b/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingPopulateService.kt new file mode 100644 index 00000000..89e0dbea --- /dev/null +++ b/batch/src/main/kotlin/lecturebuildings/service/LectureBuildingPopulateService.kt @@ -0,0 +1,126 @@ +package com.wafflestudio.snu4t.lecturebuildings.service + +import com.wafflestudio.snu4t.lecturebuildings.data.Campus +import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuilding +import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuildingUpdateResult +import com.wafflestudio.snu4t.lecturebuildings.data.PlaceInfo +import com.wafflestudio.snu4t.lecturebuildings.repository.LectureBuildingRepository +import com.wafflestudio.snu4t.lectures.data.Lecture +import com.wafflestudio.snu4t.lectures.repository.LectureRepository +import com.wafflestudio.snu4t.timetables.data.Timetable +import com.wafflestudio.snu4t.timetables.repository.TimetableRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Service +import java.time.Instant + +interface LectureBuildingPopulateService { + suspend fun populateLectureBuildingsWithFetch(lectures: List): LectureBuildingUpdateResult + suspend fun populateLectureBuildingsOfTimetables(lecture: Lecture): List +} + +@Service +class LectureBuildingPopulateServiceImpl( + private val lectureBuildingFetchService: LectureBuildingFetchService, + private val lectureRepository: LectureRepository, + private val lectureBuildingRepository: LectureBuildingRepository, + private val timetableRepository: TimetableRepository, +) : LectureBuildingPopulateService { + // 빌딩 정보를 조회하고 없으면 크롤링해옴 + + override suspend fun populateLectureBuildingsWithFetch(lectures: List): LectureBuildingUpdateResult { + val lecturePlaceInfos = lectures.flatMap { it.classPlaceAndTimes } + .map { it.place }.distinct() + .flatMap { PlaceInfo.getValuesOf(it) } + + val lectureBuildingNumbers = lecturePlaceInfos.map { it.buildingNumber }.toSet() + val lecturePlaceInfoMap = lecturePlaceInfos.associateBy { it.rawString } + + val savedLectureBuildings = lectureBuildingRepository.findByBuildingNumberIsIn(lectureBuildingNumbers) + val savedBuildingNumbers = savedLectureBuildings.map { it.buildingNumber }.toSet() + + // 존재하지 않는 강의동을 캠퍼스맵에서 긁어와서 저장 + val buildingNumbersToFetch = lectureBuildingNumbers - savedBuildingNumbers + val fetchedBuildlingInfo = fetchBuildings(buildingNumbersToFetch) + val fetchedLectureBuildings = lectureBuildingRepository.saveAll(fetchedBuildlingInfo).toList() + + // Lecture에 강의동 정보를 추가 + val updateResult = updateBuildingInfoOfLectures(lectures, lecturePlaceInfoMap, savedLectureBuildings + fetchedLectureBuildings) + val results = lectureRepository.saveAll(updateResult.lecturesWithBuildingInfos).toList() + + return updateResult + } + + private suspend fun fetchBuildings(buildingNumbers: Set): List = runBlocking { + return@runBlocking buildingNumbers.map { + async { + return@async lectureBuildingFetchService.getSnuMapLectureBuilding( + Campus.GWANAK, + it + ) + }.await() + } + }.filterNotNull() + + private fun updateBuildingInfoOfLectures( + lectures: List, + placeInfoMap: Map, + lectureBuildings: List + ): LectureBuildingUpdateResult { + val lectureBuildingMap = lectureBuildings.associateBy { it.buildingNumber } + + val infoPopulated: MutableList = mutableListOf() + val infoEmpty: MutableList = mutableListOf() + val infoPopulationFailed: MutableList = mutableListOf() + + for (lecture in lectures) { + if (lecture.classPlaceAndTimes.isEmpty() || + lecture.classPlaceAndTimes.map { it.place }.joinToString("").isBlank() + ) { + infoEmpty.add(lecture) + continue + } + + val newClassPlaceAndTimes = lecture.classPlaceAndTimes.map { + it.apply { + this.lectureBuildings = PlaceInfo.getValuesOf(it.place) + .mapNotNull { placeInfoMap[it.rawString] } + .distinctBy { it.buildingNumber } + .mapNotNull { lectureBuildingMap[it.buildingNumber] } + } + } + + val succeeded = newClassPlaceAndTimes.map { it.place.isBlank() || it.lectureBuildings != null } + .reduce { acc, b -> acc && b } + + if (succeeded) { + infoPopulated.add(lecture) + } else { + infoPopulationFailed.add(lecture) + } + } + + return LectureBuildingUpdateResult( + lecturesWithBuildingInfos = infoPopulated, + lecturesWithOutBuildingInfos = infoEmpty, + lecturesFailed = infoPopulationFailed + ) + } + + override suspend fun populateLectureBuildingsOfTimetables(lecture: Lecture): List { + val updatedTimetables = timetableRepository.findAllContainsLectureId(lecture.year, lecture.semester, lecture.id!!) + .map { timetable -> + timetable.apply { + lectures.find { it.lectureId == lecture.id && it.classPlaceAndTimes == lecture.classPlaceAndTimes }?.apply { + classPlaceAndTimes = lecture.classPlaceAndTimes + } + updatedAt = Instant.now() + } + } + .toList() + + return timetableRepository.saveAll(updatedTimetables).toList() + } +} diff --git a/batch/src/main/kotlin/sugangsnu/common/utils/SugangSnuClassTimeUtils.kt b/batch/src/main/kotlin/sugangsnu/common/utils/SugangSnuClassTimeUtils.kt index 6cc0cb5f..43b5825b 100644 --- a/batch/src/main/kotlin/sugangsnu/common/utils/SugangSnuClassTimeUtils.kt +++ b/batch/src/main/kotlin/sugangsnu/common/utils/SugangSnuClassTimeUtils.kt @@ -29,7 +29,7 @@ object SugangSnuClassTimeUtils { place = locationTexts.joinToString("/"), startMinute = sugangSnuClassTime.startHour.toInt() * 60 + sugangSnuClassTime.startMinute.toInt(), endMinute = sugangSnuClassTime.endHour.toInt() * 60 + sugangSnuClassTime.endMinute.toInt(), - lectureBuilding = null + lectureBuildings = null ) } .sortedWith(compareBy({ it.day.value }, { it.startMinute })) diff --git a/batch/src/main/kotlin/sugangsnu/job/sync/data/UpdatedLecture.kt b/batch/src/main/kotlin/sugangsnu/job/sync/data/UpdatedLecture.kt index 73de33b9..a9e68d85 100644 --- a/batch/src/main/kotlin/sugangsnu/job/sync/data/UpdatedLecture.kt +++ b/batch/src/main/kotlin/sugangsnu/job/sync/data/UpdatedLecture.kt @@ -5,6 +5,6 @@ import kotlin.reflect.KProperty1 class UpdatedLecture( val oldData: Lecture, - val newData: Lecture, + var newData: Lecture, val updatedField: List> ) diff --git a/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt b/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt index 4166ac86..010f865e 100644 --- a/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt +++ b/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt @@ -4,6 +4,8 @@ import com.wafflestudio.snu4t.bookmark.repository.BookmarkRepository import com.wafflestudio.snu4t.common.cache.Cache import com.wafflestudio.snu4t.coursebook.data.Coursebook import com.wafflestudio.snu4t.coursebook.repository.CoursebookRepository +import com.wafflestudio.snu4t.lecturebuildings.data.PlaceInfo +import com.wafflestudio.snu4t.lecturebuildings.repository.LectureBuildingRepository import com.wafflestudio.snu4t.lectures.data.Lecture import com.wafflestudio.snu4t.lectures.service.LectureService import com.wafflestudio.snu4t.lectures.utils.ClassTimeUtils @@ -48,16 +50,18 @@ class SugangSnuSyncServiceImpl( private val coursebookRepository: CoursebookRepository, private val bookmarkRepository: BookmarkRepository, private val tagListRepository: TagListRepository, + private val lectureBuildingRepository: LectureBuildingRepository, private val cache: Cache, ) : SugangSnuSyncService { override suspend fun updateCoursebook(coursebook: Coursebook): List { val newLectures = sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester) val oldLectures = lectureService.getLecturesByYearAndSemesterAsFlow(coursebook.year, coursebook.semester).toList() - val compareResult = compareLectures(newLectures, oldLectures) + var compareResult = compareLectures(newLectures, oldLectures) + val compareResultWithBuildingInfo = addBuildingInfos(compareResult) - syncLectures(compareResult) - val syncUserLecturesResults = syncSavedUserLectures(compareResult) + syncLectures(compareResultWithBuildingInfo) + val syncUserLecturesResults = syncSavedUserLectures(compareResultWithBuildingInfo) syncTagList(coursebook, newLectures) coursebookRepository.save(coursebook.apply { updatedAt = Instant.now() }) @@ -145,6 +149,47 @@ class SugangSnuSyncServiceImpl( lectureService.deleteLectures(compareResult.deletedLectureList) } + private suspend fun addBuildingInfos(compareResult: SugangSnuLectureCompareResult): SugangSnuLectureCompareResult { + val createdLectureListWithBuildingInfo = populateLecturesWithBuildingInfo(compareResult.createdLectureList) + + val updatedLectures = compareResult.updatedLectureList.map { it.newData } + val updatedLecturesWithBuildingInfo = populateLecturesWithBuildingInfo(updatedLectures) + .associateBy { it.id } + val updatedLectureList = compareResult.updatedLectureList.map { + it.apply { + this.newData = this.newData.id?.let { updatedLecturesWithBuildingInfo[it] } ?: this.newData + } + } + + return SugangSnuLectureCompareResult( + createdLectureList = createdLectureListWithBuildingInfo, + deletedLectureList = compareResult.deletedLectureList, + updatedLectureList = updatedLectureList + ) + } + + // 미리 저장해둔 빌딩 정보로 강의에 lectureBuilding 정보 추가 + private suspend fun populateLecturesWithBuildingInfo(lectures: List): List { + val buildingNumbers = lectures + .flatMap { it.classPlaceAndTimes } + .map { PlaceInfo(it.place)?.buildingNumber } + .filterNotNull() + + val buildingsMap = lectureBuildingRepository.findByBuildingNumberIsIn(buildingNumbers.toSet()) + .associateBy { it.buildingNumber } + + return lectures.map { lecture -> + lecture.apply { + this.classPlaceAndTimes = lecture.classPlaceAndTimes.map { classPlaceAndTime -> + classPlaceAndTime.apply { + this.lectureBuildings = PlaceInfo.getValuesOf(place) + .mapNotNull { buildingsMap[it.buildingNumber] } + } + } + } + } + } + private suspend fun syncSavedUserLectures(compareResult: SugangSnuLectureCompareResult): List = merge( syncTimetableLectures(compareResult), diff --git a/core/src/main/kotlin/lecturebuildings/data/Campus.kt b/core/src/main/kotlin/lecturebuildings/data/Campus.kt new file mode 100644 index 00000000..93de4b06 --- /dev/null +++ b/core/src/main/kotlin/lecturebuildings/data/Campus.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.snu4t.lecturebuildings.data + +enum class Campus { + GWANAK, + YEONGEON, + PYEONGCHANG, +} diff --git a/core/src/main/kotlin/lecturebuildings/data/GeoCoordinate.kt b/core/src/main/kotlin/lecturebuildings/data/GeoCoordinate.kt new file mode 100644 index 00000000..b4c64b76 --- /dev/null +++ b/core/src/main/kotlin/lecturebuildings/data/GeoCoordinate.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.snu4t.lecturebuildings.data + +class GeoCoordinate( + val latitude: Double, + val longitude: Double +) diff --git a/core/src/main/kotlin/lecturebuildings/data/LectureBuilding.kt b/core/src/main/kotlin/lecturebuildings/data/LectureBuilding.kt new file mode 100644 index 00000000..fdd4f888 --- /dev/null +++ b/core/src/main/kotlin/lecturebuildings/data/LectureBuilding.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.snu4t.lecturebuildings.data + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document + +@Document +data class LectureBuilding( + @Id + val id: String? = null, + + // 동 + val buildingNumber: String, + + // 건물 이름(한국어) - 자연과학대학(500) + val buildingNameKor: String? = null, + + // 건물 이름(영어) - College of Natural Sciences(500) + val buildingNameEng: String? = null, + + // 위경도 - 37.4592190840394, 126.948120067187 + val locationInDMS: GeoCoordinate?, + + // 위경도(십진표기) - 488525, 1099948 + val locationInDecimal: GeoCoordinate?, + + // 캠퍼스 - 관악 + val campus: Campus, +) diff --git a/core/src/main/kotlin/lecturebuildings/repository/LectureBuildingRepository.kt b/core/src/main/kotlin/lecturebuildings/repository/LectureBuildingRepository.kt new file mode 100644 index 00000000..78c0ad7a --- /dev/null +++ b/core/src/main/kotlin/lecturebuildings/repository/LectureBuildingRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.snu4t.lecturebuildings.repository + +import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuilding +import org.springframework.data.repository.kotlin.CoroutineCrudRepository + +interface LectureBuildingRepository : CoroutineCrudRepository { + suspend fun findByBuildingNumber(buildingNumber: String): LectureBuilding? + suspend fun findByBuildingNumberIsIn(buildingNumbers: Set): List +} diff --git a/core/src/main/kotlin/lectures/data/ClassPlaceAndTime.kt b/core/src/main/kotlin/lectures/data/ClassPlaceAndTime.kt index d925b831..09c23279 100644 --- a/core/src/main/kotlin/lectures/data/ClassPlaceAndTime.kt +++ b/core/src/main/kotlin/lectures/data/ClassPlaceAndTime.kt @@ -1,12 +1,26 @@ package com.wafflestudio.snu4t.lectures.data import com.wafflestudio.snu4t.common.enum.DayOfWeek -import com.wafflestudio.snu4t.lecturehalls.data.LectureBuilding +import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuilding data class ClassPlaceAndTime( val day: DayOfWeek, val place: String, val startMinute: Int, val endMinute: Int, - var lectureBuilding: LectureBuilding? -) + var lectureBuildings: List? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + return when (other) { + is ClassPlaceAndTime -> { + this.day == other.day && + this.place == other.place && + this.startMinute == other.startMinute && + this.endMinute == other.endMinute + } + + else -> false + } + } +} diff --git a/core/src/main/kotlin/lectures/dto/ClassPlaceAndTimeDto.kt b/core/src/main/kotlin/lectures/dto/ClassPlaceAndTimeDto.kt index 5c8d6a72..a1014184 100644 --- a/core/src/main/kotlin/lectures/dto/ClassPlaceAndTimeDto.kt +++ b/core/src/main/kotlin/lectures/dto/ClassPlaceAndTimeDto.kt @@ -2,14 +2,11 @@ package com.wafflestudio.snu4t.lectures.dto import com.fasterxml.jackson.annotation.JsonProperty import com.wafflestudio.snu4t.common.enum.DayOfWeek -import com.wafflestudio.snu4t.lecturehalls.data.Campus -import com.wafflestudio.snu4t.lecturehalls.data.GeoCoordinate -import com.wafflestudio.snu4t.lecturehalls.data.LectureBuilding +import com.wafflestudio.snu4t.lecturebuildings.data.LectureBuilding import com.wafflestudio.snu4t.lectures.data.ClassPlaceAndTime import com.wafflestudio.snu4t.lectures.utils.endPeriod import com.wafflestudio.snu4t.lectures.utils.minuteToString import com.wafflestudio.snu4t.lectures.utils.startPeriod -import org.bson.types.ObjectId data class ClassPlaceAndTimeDto( val day: DayOfWeek, @@ -38,7 +35,7 @@ data class ClassPlaceAndTimeLegacyDto( val periodLength: Double, @JsonProperty("start") val startPeriod: Double, - val lectureBuilding: LectureBuilding? + val lectureBuildings: List?, ) fun ClassPlaceAndTimeLegacyDto(classPlaceAndTime: ClassPlaceAndTime): ClassPlaceAndTimeLegacyDto = ClassPlaceAndTimeLegacyDto( @@ -50,15 +47,5 @@ fun ClassPlaceAndTimeLegacyDto(classPlaceAndTime: ClassPlaceAndTime): ClassPlace endTime = minuteToString(classPlaceAndTime.endMinute), startPeriod = classPlaceAndTime.startPeriod, periodLength = classPlaceAndTime.endPeriod - classPlaceAndTime.startPeriod, - lectureBuilding = MockLectureBuilding() -) - -private fun MockLectureBuilding() = LectureBuilding( - id = ObjectId.get().toHexString(), - buildingNumber = "500", - buildingNameKor = "자연과학대학(500)", - buildingNameEng = "College of Natural Sciences(500)", - locationInDMS = GeoCoordinate(37.4592190840394, 126.94812006718699), - locationInDecimal = GeoCoordinate(488525.0, 1099948.0), - campus = Campus.GWANAK + lectureBuildings = classPlaceAndTime.lectureBuildings ) diff --git a/core/src/main/kotlin/timetables/dto/request/ClassPlaceAndTimeLegacyRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/ClassPlaceAndTimeLegacyRequestDto.kt index 1f0b1649..1adcb35b 100644 --- a/core/src/main/kotlin/timetables/dto/request/ClassPlaceAndTimeLegacyRequestDto.kt +++ b/core/src/main/kotlin/timetables/dto/request/ClassPlaceAndTimeLegacyRequestDto.kt @@ -39,8 +39,7 @@ data class ClassPlaceAndTimeLegacyRequestDto( day = day, place = place ?: "", startMinute = startMinute, - endMinute = endMinute, - lectureBuilding = null + endMinute = endMinute ) }