Skip to content

Commit 2cebfb1

Browse files
authored
Merge pull request #337 from wafflestudio/develop
Release
2 parents 2bcfdc8 + 2d8aed7 commit 2cebfb1

File tree

22 files changed

+193
-22
lines changed

22 files changed

+193
-22
lines changed

api/src/main/kotlin/handler/LectureSearchHandler.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ data class SearchQueryLegacy(
4848
val offset: Long = page * 20L,
4949
val limit: Int = 20,
5050
val sortCriteria: String? = null,
51+
val categoryPre2025: List<String>? = null,
5152
) {
5253
fun toSearchDto(): SearchDto {
5354
return SearchDto(
@@ -67,6 +68,7 @@ data class SearchQueryLegacy(
6768
offset = offset,
6869
limit = limit,
6970
sortBy = sortCriteria,
71+
categoryPre2025 = categoryPre2025,
7072
)
7173
}
7274

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.wafflestudio.snu4t.pre2025category.api
2+
3+
import org.springframework.context.annotation.Bean
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.http.client.reactive.ReactorClientHttpConnector
6+
import org.springframework.web.reactive.function.client.ExchangeStrategies
7+
import org.springframework.web.reactive.function.client.WebClient
8+
import reactor.netty.http.client.HttpClient
9+
10+
@Configuration
11+
class GoogleDocsApiConfig {
12+
companion object {
13+
const val GOOGLE_DOCS_BASE_URL = "https://docs.google.com"
14+
}
15+
16+
@Bean
17+
fun googleDocsApi(): GoogleDocsApi {
18+
val exchangeStrategies: ExchangeStrategies =
19+
ExchangeStrategies.builder()
20+
.codecs { it.defaultCodecs().maxInMemorySize(-1) } // to unlimited memory size
21+
.build()
22+
23+
val httpClient =
24+
HttpClient.create()
25+
.followRedirect(true)
26+
.compress(true)
27+
28+
return WebClient.builder()
29+
.baseUrl(GOOGLE_DOCS_BASE_URL)
30+
.clientConnector(ReactorClientHttpConnector(httpClient))
31+
.exchangeStrategies(exchangeStrategies)
32+
.build()
33+
.let(::GoogleDocsApi)
34+
}
35+
}
36+
37+
class GoogleDocsApi(webClient: WebClient) : WebClient by webClient
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.wafflestudio.snu4t.pre2025category.repository
2+
3+
import com.wafflestudio.snu4t.pre2025category.api.GoogleDocsApi
4+
import org.springframework.core.io.buffer.PooledDataBuffer
5+
import org.springframework.http.MediaType
6+
import org.springframework.stereotype.Component
7+
import org.springframework.web.reactive.function.client.awaitBody
8+
import org.springframework.web.reactive.function.client.awaitExchange
9+
import org.springframework.web.reactive.function.client.createExceptionAndAwait
10+
11+
@Component
12+
class CategoryPre2025Repository(
13+
private val googleDocsApi: GoogleDocsApi,
14+
) {
15+
companion object {
16+
const val SPREADSHEET_PATH = "/spreadsheets/d"
17+
const val SPREADSHEET_KEY = "/1Ok2gu7rW1VYlKmC_zSjNmcljef0kstm19P9zJ_5s_QA"
18+
}
19+
20+
suspend fun fetchCategoriesPre2025(): PooledDataBuffer =
21+
googleDocsApi.get().uri { builder ->
22+
builder.run {
23+
path(SPREADSHEET_PATH)
24+
path(SPREADSHEET_KEY)
25+
path("/export")
26+
queryParam("format", "xlsx")
27+
build()
28+
}
29+
}.accept(MediaType.TEXT_HTML).awaitExchange {
30+
if (it.statusCode().is2xxSuccessful) {
31+
it.awaitBody()
32+
} else {
33+
throw it.createExceptionAndAwait()
34+
}
35+
}
36+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.wafflestudio.snu4t.pre2025category.service
2+
3+
import com.wafflestudio.snu4t.pre2025category.repository.CategoryPre2025Repository
4+
import org.apache.poi.ss.usermodel.WorkbookFactory
5+
import org.springframework.stereotype.Service
6+
7+
@Service
8+
class CategoryPre2025FetchService(
9+
private val categoryPre2025Repository: CategoryPre2025Repository,
10+
) {
11+
suspend fun getCategoriesPre2025(): Map<String, String> {
12+
val oldCategoriesXlsx = categoryPre2025Repository.fetchCategoriesPre2025()
13+
val workbook = WorkbookFactory.create(oldCategoriesXlsx.asInputStream())
14+
return workbook.sheetIterator().asSequence()
15+
.flatMap { sheet ->
16+
sheet.rowIterator().asSequence()
17+
.drop(4)
18+
.map { row ->
19+
try {
20+
val currentCourseNumber = row.getCell(7).stringCellValue
21+
val oldCategory = row.getCell(1).stringCellValue
22+
if (currentCourseNumber.isBlank() || oldCategory.isBlank()) {
23+
return@map null
24+
}
25+
currentCourseNumber to oldCategory
26+
} catch (e: Exception) {
27+
null
28+
}
29+
}
30+
.filterNotNull()
31+
}
32+
.toMap()
33+
}
34+
}

batch/src/main/kotlin/sugangsnu/common/service/SugangSnuFetchService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class SugangSnuFetchServiceImpl(
136136
category = "",
137137
classPlaceAndTimes = classTimes,
138138
registrationCount = registrationCount,
139+
categoryPre2025 = null,
139140
)
140141
}
141142
}

batch/src/main/kotlin/sugangsnu/common/utils/SugangSnuExtensions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ fun KProperty1<Lecture, *>.toKoreanFieldName(): String =
1515
Lecture::instructor -> "교수"
1616
Lecture::quota -> "정원"
1717
Lecture::remark -> "비고"
18-
Lecture::category -> "교양 구분"
18+
Lecture::category -> "교양영역"
1919
Lecture::classPlaceAndTimes -> "강의 시간/장소"
20+
Lecture::categoryPre2025 -> "구) 교양영역"
2021
else -> "기타"
2122
}
2223

batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.wafflestudio.snu4t.sugangsnu.job.sync.service
22

33
import com.wafflestudio.snu4t.bookmark.repository.BookmarkRepository
4-
import com.wafflestudio.snu4t.common.cache.Cache
54
import com.wafflestudio.snu4t.coursebook.data.Coursebook
65
import com.wafflestudio.snu4t.coursebook.repository.CoursebookRepository
76
import com.wafflestudio.snu4t.lecturebuildings.data.Campus
@@ -10,6 +9,7 @@ import com.wafflestudio.snu4t.lecturebuildings.service.LectureBuildingService
109
import com.wafflestudio.snu4t.lectures.data.Lecture
1110
import com.wafflestudio.snu4t.lectures.service.LectureService
1211
import com.wafflestudio.snu4t.lectures.utils.ClassTimeUtils
12+
import com.wafflestudio.snu4t.pre2025category.service.CategoryPre2025FetchService
1313
import com.wafflestudio.snu4t.sugangsnu.common.SugangSnuRepository
1414
import com.wafflestudio.snu4t.sugangsnu.common.data.SugangSnuCoursebookCondition
1515
import com.wafflestudio.snu4t.sugangsnu.common.service.SugangSnuFetchService
@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.filter
3131
import kotlinx.coroutines.flow.map
3232
import kotlinx.coroutines.flow.merge
3333
import kotlinx.coroutines.flow.toList
34+
import org.slf4j.LoggerFactory
3435
import org.springframework.stereotype.Service
3536
import java.time.Instant
3637
import kotlin.reflect.full.memberProperties
@@ -46,32 +47,48 @@ interface SugangSnuSyncService {
4647
@Service
4748
class SugangSnuSyncServiceImpl(
4849
private val sugangSnuFetchService: SugangSnuFetchService,
50+
private val categoryPre2025FetchService: CategoryPre2025FetchService,
4951
private val lectureService: LectureService,
5052
private val timeTableRepository: TimetableRepository,
5153
private val sugangSnuRepository: SugangSnuRepository,
5254
private val coursebookRepository: CoursebookRepository,
5355
private val bookmarkRepository: BookmarkRepository,
5456
private val tagListRepository: TagListRepository,
5557
private val lectureBuildingService: LectureBuildingService,
56-
private val cache: Cache,
5758
) : SugangSnuSyncService {
59+
private val log = LoggerFactory.getLogger(javaClass)
60+
5861
override suspend fun updateCoursebook(coursebook: Coursebook): List<UserLectureSyncResult> {
59-
val newLectures = sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester)
62+
val courseNumberCategoryPre2025Map = categoryPre2025FetchService.getCategoriesPre2025()
63+
val newLectures =
64+
sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester)
65+
.map { lecture ->
66+
lecture.apply {
67+
categoryPre2025 = courseNumberCategoryPre2025Map[lecture.courseNumber]
68+
}
69+
}
6070
val oldLectures =
6171
lectureService.getLecturesByYearAndSemesterAsFlow(coursebook.year, coursebook.semester).toList()
6272
val compareResult = compareLectures(newLectures, oldLectures)
6373

6474
syncLectures(compareResult)
65-
updateLectureBuildings(compareResult)
6675
val syncUserLecturesResults = syncSavedUserLectures(compareResult)
6776
syncTagList(coursebook, newLectures)
6877
coursebookRepository.save(coursebook.apply { updatedAt = Instant.now() })
78+
runCatching { updateLectureBuildings(compareResult) }.onFailure { log.error("Failed to update lecture buildings", it) }
6979

7080
return syncUserLecturesResults
7181
}
7282

7383
override suspend fun addCoursebook(coursebook: Coursebook) {
74-
val newLectures = sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester)
84+
val courseNumberCategoryPre2025Map = categoryPre2025FetchService.getCategoriesPre2025()
85+
val newLectures =
86+
sugangSnuFetchService.getSugangSnuLectures(coursebook.year, coursebook.semester)
87+
.map { lecture ->
88+
lecture.apply {
89+
categoryPre2025 = courseNumberCategoryPre2025Map[lecture.courseNumber]
90+
}
91+
}
7592
lectureService.upsertLectures(newLectures)
7693
syncTagList(coursebook, newLectures)
7794

@@ -125,6 +142,7 @@ class SugangSnuSyncServiceImpl(
125142
credit = acc.credit + lecture.credit,
126143
instructor = acc.instructor + lecture.instructor,
127144
category = acc.category + lecture.category,
145+
categoryPre2025 = acc.categoryPre2025 + lecture.categoryPre2025,
128146
)
129147
}.let { parsedTag ->
130148
TagCollection(
@@ -135,6 +153,7 @@ class SugangSnuSyncServiceImpl(
135153
credit = parsedTag.credit.sorted().map { "${it}학점" },
136154
instructor = parsedTag.instructor.filterNotNull().filter { it.isNotBlank() }.sorted(),
137155
category = parsedTag.category.filterNotNull().filter { it.isNotBlank() }.sorted(),
156+
categoryPre2025 = parsedTag.categoryPre2025.filterNotNull().filter { it.isNotBlank() }.sorted(),
138157
)
139158
}
140159
val tagList =
@@ -182,8 +201,8 @@ class SugangSnuSyncServiceImpl(
182201
updatedLecture.oldData.semester,
183202
updatedLecture.oldData.id!!,
184203
).map { bookmark ->
185-
bookmark.apply {
186-
lectures.find { it.id == updatedLecture.oldData.id }?.apply {
204+
val updatedBookmarkLecture =
205+
bookmark.lectures.find { it.id == updatedLecture.oldData.id }?.apply {
187206
academicYear = updatedLecture.newData.academicYear
188207
category = updatedLecture.newData.category
189208
classPlaceAndTimes = updatedLecture.newData.classPlaceAndTimes
@@ -197,10 +216,9 @@ class SugangSnuSyncServiceImpl(
197216
lectureNumber = updatedLecture.newData.lectureNumber
198217
courseNumber = updatedLecture.newData.courseNumber
199218
courseTitle = updatedLecture.newData.courseTitle
200-
}
201-
}
202-
}.let {
203-
bookmarkRepository.saveAll(it)
219+
categoryPre2025 = updatedLecture.newData.categoryPre2025
220+
}!!
221+
bookmarkRepository.updateLecture(bookmark.id!!, updatedBookmarkLecture)
204222
}.map { bookmark ->
205223
BookmarkLectureUpdateResult(
206224
bookmark.year,
@@ -235,8 +253,8 @@ class SugangSnuSyncServiceImpl(
235253
updatedLecture: UpdatedLecture,
236254
): Flow<TimetableLectureUpdateResult> =
237255
timetables.map { timetable ->
238-
timetable.apply {
239-
lectures.find { it.lectureId == updatedLecture.oldData.id }?.apply {
256+
val updatedTimetableLecture =
257+
timetable.lectures.find { it.lectureId == updatedLecture.oldData.id }?.apply {
240258
academicYear = updatedLecture.newData.academicYear
241259
category = updatedLecture.newData.category
242260
classPlaceAndTimes = updatedLecture.newData.classPlaceAndTimes
@@ -250,11 +268,9 @@ class SugangSnuSyncServiceImpl(
250268
remark = updatedLecture.newData.remark
251269
courseNumber = updatedLecture.newData.courseNumber
252270
courseTitle = updatedLecture.newData.courseTitle
271+
categoryPre2025 = updatedLecture.newData.categoryPre2025
253272
}
254-
updatedAt = Instant.now()
255-
}
256-
}.let {
257-
timeTableRepository.saveAll(it)
273+
timeTableRepository.updateTimetableLecture(timetable.id!!, updatedTimetableLecture!!)
258274
}.map { timetable ->
259275
TimetableLectureUpdateResult(
260276
year = timetable.year,
@@ -347,4 +363,5 @@ data class ParsedTags(
347363
val instructor: Set<String?> = setOf(),
348364
val category: Set<String?> = setOf(),
349365
val etc: Set<String?> = setOf(),
366+
val categoryPre2025: Set<String?> = setOf(),
350367
)

core/src/main/kotlin/bookmark/repository/BookmarkCustomRepository.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.wafflestudio.snu4t.lectures.data.BookmarkLecture
88
import com.wafflestudio.snu4t.lectures.data.Lecture
99
import kotlinx.coroutines.flow.Flow
1010
import kotlinx.coroutines.reactive.asFlow
11+
import org.bson.types.ObjectId
1112
import org.springframework.data.mapping.toDotPath
1213
import org.springframework.data.mongodb.core.FindAndModifyOptions
1314
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
@@ -43,6 +44,11 @@ interface BookmarkCustomRepository {
4344
timeTableId: String,
4445
lectureId: String,
4546
)
47+
48+
suspend fun updateLecture(
49+
bookmarkId: String,
50+
lecture: BookmarkLecture,
51+
): Bookmark
4652
}
4753

4854
class BookmarkCustomRepositoryImpl(private val reactiveMongoTemplate: ReactiveMongoTemplate) :
@@ -101,4 +107,14 @@ class BookmarkCustomRepositoryImpl(private val reactiveMongoTemplate: ReactiveMo
101107
Update().pull(Bookmark::lectures.toDotPath(), Query.query(BookmarkLecture::id isEqualTo lectureId)),
102108
).findModifyAndAwait()
103109
}
110+
111+
override suspend fun updateLecture(
112+
bookmarkId: String,
113+
lecture: BookmarkLecture,
114+
): Bookmark {
115+
return reactiveMongoTemplate.update<Bookmark>().matching(
116+
Bookmark::id.isEqualTo(bookmarkId).and("lectures._id").isEqualTo(ObjectId(lecture.id)),
117+
).apply(Update().set("""lectures.$""", lecture))
118+
.withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait()
119+
}
104120
}

core/src/main/kotlin/lectures/data/BookmarkLecture.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ data class BookmarkLecture(
3030
var courseNumber: String,
3131
@Field("course_title")
3232
var courseTitle: String,
33+
var categoryPre2025: String?,
3334
)
3435

3536
fun BookmarkLecture(lecture: Lecture): BookmarkLecture =
@@ -48,4 +49,5 @@ fun BookmarkLecture(lecture: Lecture): BookmarkLecture =
4849
lectureNumber = lecture.lectureNumber,
4950
courseNumber = lecture.courseNumber,
5051
courseTitle = lecture.courseTitle,
52+
categoryPre2025 = lecture.categoryPre2025,
5153
)

core/src/main/kotlin/lectures/data/Lecture.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ data class Lecture(
3737
var registrationCount: Int = 0,
3838
var wasFull: Boolean = false,
3939
val evInfo: EvInfo? = null,
40+
var categoryPre2025: String?,
4041
) {
4142
infix fun equalsMetadata(other: Lecture): Boolean {
4243
return this === other ||
@@ -54,6 +55,7 @@ data class Lecture(
5455
semester == other.semester &&
5556
year == other.year &&
5657
courseNumber == other.courseNumber &&
57-
courseTitle == other.courseTitle
58+
courseTitle == other.courseTitle &&
59+
categoryPre2025 == other.categoryPre2025
5860
}
5961
}

0 commit comments

Comments
 (0)