Skip to content

Commit 977e596

Browse files
authored
Merge pull request #511 from wafflestudio/develop
Release
2 parents ccf65b6 + 96aa3d1 commit 977e596

12 files changed

Lines changed: 70 additions & 21 deletions

File tree

api/src/test/kotlin/timetable/TimetableIntegTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.wafflestudio.snutt.timetable
33
import BaseIntegTest
44
import com.ninjasquad.springmockk.MockkBean
55
import com.wafflestudio.snutt.config.USER_ATTRIBUTE_KEY
6+
import com.wafflestudio.snutt.coursebook.service.CoursebookService
67
import com.wafflestudio.snutt.evaluation.service.EvService
78
import com.wafflestudio.snutt.filter.ApiKeyWebFilter
89
import com.wafflestudio.snutt.filter.UserAuthenticationWebFilter
@@ -29,6 +30,7 @@ import timetables.dto.TimetableBriefDto
2930
@AutoConfigureWebTestClient
3031
class TimetableIntegTest(
3132
@MockkBean private val mockSnuttEvService: EvService,
33+
@MockkBean private val mockCoursebookService: CoursebookService,
3234
@MockkBean private val apiKeyWebFilter: ApiKeyWebFilter,
3335
@MockkBean private val userAuthenticationWebFilter: UserAuthenticationWebFilter,
3436
val webTestClient: WebTestClient,
@@ -39,6 +41,7 @@ class TimetableIntegTest(
3941
) : BaseIntegTest({
4042
coEvery { mockSnuttEvService.getSummariesByIds(any()) } returns emptyList()
4143
coEvery { mockSnuttEvService.getEvIdsBySnuttIds(any()) } returns emptyList()
44+
coEvery { mockCoursebookService.existsCoursebook(any(), any()) } returns true
4245
coEvery {
4346
apiKeyWebFilter.filter(any(), any())
4447
} answers {

batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,19 @@ class SugangSnuRepository(
5151
""".trimIndent().replace("\n", "")
5252
}
5353

54-
suspend fun getSearchPageHtml(pageNo: Int = 1): PooledDataBuffer =
54+
suspend fun getSearchPageHtml(
55+
year: Int,
56+
semester: Semester,
57+
pageNo: Int = 1,
58+
): PooledDataBuffer =
5559
sugangSnuApi
5660
.get()
5761
.uri { builder ->
5862
builder
5963
.path(SUGANG_SNU_SEARCH_PATH)
6064
.query(DEFAULT_SEARCH_PAGE_PARAMS)
65+
.queryParam("srchOpenSchyy", year)
66+
.queryParam("srchOpenShtm", convertSemesterToSugangSnuSearchString(semester))
6167
.queryParam("pageNo", pageNo)
6268
.build()
6369
}.accept(MediaType.TEXT_HTML)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object RegistrationPeriodParseUtils {
2828

2929
private fun parseRegistrationPhase(typeText: String): RegistrationPhase? =
3030
when {
31+
typeText.contains("예비") -> null
3132
typeText.contains("전산확정") -> null
3233
typeText.contains("정원외") -> null
3334
typeText.contains("수강취소") -> null

batch/src/main/kotlin/sugangsnu/job/vacancynotification/service/VacancyNotifierService.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class VacancyNotifierServiceImpl(
6565
log.info("시작: ${registrationPhase.name}")
6666
val pageCount =
6767
runCatching {
68-
getPageCount()
68+
getPageCount(coursebook)
6969
}.getOrElse {
7070
log.error("에러가 발생했거나 부하 기간입니다. {}", it.message, it)
7171
delay(30L.seconds)
@@ -77,7 +77,7 @@ class VacancyNotifierServiceImpl(
7777

7878
// 수강 사이트 부하를 분산하기 위해 강의 전체를 20등분해서 각각 요청
7979
(1..pageCount).chunked(pageCount / 20).forEach { chunkedPages ->
80-
val registrationStatus = getRegistrationStatus(chunkedPages)
80+
val registrationStatus = getRegistrationStatus(chunkedPages, coursebook)
8181
if (registrationStatus.all { it.registrationCount == 0 }) return VacancyNotificationJobResult.REGISTRATION_IS_NOT_STARTED
8282

8383
val registrationStatusMap =
@@ -157,19 +157,22 @@ class VacancyNotifierServiceImpl(
157157
this.quota == this.registrationCount
158158
}
159159

160-
private suspend fun getPageCount(): Int {
161-
val firstPageContent = getSugangSnuSearchContent(1)
160+
private suspend fun getPageCount(coursebook: Coursebook): Int {
161+
val firstPageContent = getSugangSnuSearchContent(1, coursebook)
162162
val totalCount =
163163
firstPageContent.select("div.content > div.search-result-con > small > em").text().toInt()
164164
return (totalCount + 9) / COUNT_PER_PAGE
165165
}
166166

167-
private suspend fun getRegistrationStatus(pages: List<Int>): List<RegistrationStatus> =
167+
private suspend fun getRegistrationStatus(
168+
pages: List<Int>,
169+
coursebook: Coursebook,
170+
): List<RegistrationStatus> =
168171
supervisorScope {
169172
pages
170173
.map { page ->
171174
async {
172-
getSugangSnuSearchContent(page).extractRegistrationStatus()
175+
getSugangSnuSearchContent(page, coursebook).extractRegistrationStatus()
173176
}
174177
}.awaitAll()
175178
.flatten()
@@ -211,8 +214,16 @@ class VacancyNotifierServiceImpl(
211214
}
212215
}
213216

214-
private suspend fun getSugangSnuSearchContent(pageNo: Int): Element {
215-
val webPageDataBuffer = sugangSnuRepository.getSearchPageHtml(pageNo)
217+
private suspend fun getSugangSnuSearchContent(
218+
pageNo: Int,
219+
coursebook: Coursebook,
220+
): Element {
221+
val webPageDataBuffer =
222+
sugangSnuRepository.getSearchPageHtml(
223+
year = coursebook.year,
224+
semester = coursebook.semester,
225+
pageNo = pageNo,
226+
)
216227
return try {
217228
Jsoup
218229
.parse(webPageDataBuffer.asInputStream(), Charsets.UTF_8.name(), "")

core/src/main/kotlin/common/exception/ErrorType.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum class ErrorType(
3131
CANNOT_RESET_CUSTOM_LECTURE(HttpStatus.FORBIDDEN, 0x300D, "cannot reset custom lectures", "직접 만든 강좌는 초기화할 수 없습니다"),
3232
INVALID_EMAIL(HttpStatus.FORBIDDEN, 0x300F, "email이 유효하지 않습니다", "이메일 형식이 올바르지 않습니다. 다시 입력해 주세요"),
3333
USER_EMAIL_IS_NOT_VERIFIED(HttpStatus.FORBIDDEN, 0x3011, "User email is not verified", "이메일 인증이 완료된 후 강의평을 확인할 수 있습니다", "이메일 인증 필요"),
34+
INVALID_TIMETABLE_SEMESTER(HttpStatus.FORBIDDEN, 0x300B, "시간표를 생성할 수 없는 학기입니다", "시간표를 생성할 수 없는 학기입니다"),
3435

3536
LECTURE_NOT_FOUND(HttpStatus.NOT_FOUND, 0x4003, "lecture가 없습니다", "수강편람에서 찾을 수 없는 강좌입니다"),
3637
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 0x4004, "user가 없습니다", "해당 정보로 가입된 사용자가 없습니다"),
@@ -72,6 +73,7 @@ enum class ErrorType(
7273
"현재 일시 중단된 기능입니다. 자세한 내용은 알림을 참고해 주세요.",
7374
),
7475
DIARY_QUESTION_INVALID(HttpStatus.BAD_REQUEST, 40026, "강의 일기장 질문이 유효하지 않습니다", "강의 일기장 질문이 유효하지 않습니다"),
76+
DIARY_COMMENT_TOO_LONG(HttpStatus.BAD_REQUEST, 40027, "더 남기고 싶은 말은 최대 1,000자 입력할 수 있습니다", "더 남기고 싶은 말은 최대 1,000자 입력할 수 있습니다"),
7577

7678
SOCIAL_CONNECT_FAIL(HttpStatus.UNAUTHORIZED, 40100, "소셜 로그인에 실패했습니다", "소셜 로그인에 실패했습니다"),
7779
INVALID_APPLE_LOGIN_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "유효하지 않은 애플 로그인 토큰입니다", "소셜 로그인에 실패했습니다"),
@@ -89,13 +91,14 @@ enum class ErrorType(
8991
SOCIAL_PROVIDER_NOT_ATTACHED(HttpStatus.NOT_FOUND, 40410, "소셜 계정이 연동되지 않았습니다", "소셜 계정이 연동되지 않았습니다"),
9092
DIARY_QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, 40411, "강의 일기장 질문이 유효하지 않습니다", "강의 일기장 질문이 유효하지 않습니다"),
9193
DIARY_DAILY_CLASS_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, 40412, "강의 일기장 오늘 한 일이 유효하지 않습니다", "강의 일기장 오늘 한 일이 유효하지 않습니다"),
92-
DIARY_SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, 40412, "강의 일기장 기록이 유효하지 않습니다", "강의 일기장 기록이 유효하지 않습니다"),
9394
DIARY_TARGET_LECTURE_NOT_FOUND(HttpStatus.NOT_FOUND, 40413, "강의 일기장을 작성할 강의가 없습니다", "강의 일기장을 작성할 강의가 없습니다"),
95+
DIARY_SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, 40414, "강의 일기장 기록이 유효하지 않습니다", "강의 일기장 기록이 유효하지 않습니다"),
9496
DUPLICATE_VACANCY_NOTIFICATION(HttpStatus.CONFLICT, 40900, "빈자리 알림 중복", "이미 빈자리 알림을 받고 있는 강좌입니다"),
9597
DUPLICATE_EMAIL(HttpStatus.CONFLICT, 40901, "이미 사용 중인 이메일입니다", "이미 사용 중인 이메일입니다", "회원가입 실패"),
9698
DUPLICATE_FRIEND(HttpStatus.CONFLICT, 40902, "이미 친구 관계이거나 친구 요청을 보냈습니다", "이미 친구 관계이거나 친구 요청을 보냈습니다"),
9799
INVALID_FRIEND(HttpStatus.CONFLICT, 40903, "친구 요청을 보낼 수 없는 유저입니다", "친구 요청을 보낼 수 없는 유저입니다"),
98-
DUPLICATE_THEME_NAME(HttpStatus.CONFLICT, 40904, "중복된 테마 이름입니다", "동일한 테마 이름이 이미 있습니다. 다른 이름을 입력해 주세요", "테마 이름 중복"),
100+
101+
// DUPLICATE_THEME_NAME(HttpStatus.CONFLICT, 40904, "중복된 테마 이름입니다", "동일한 테마 이름이 이미 있습니다. 다른 이름을 입력해 주세요", "테마 이름 중복"),
99102
INVALID_THEME_TYPE(HttpStatus.CONFLICT, 40905, "적절하지 않은 유형의 테마입니다", "적절하지 않은 유형의 테마입니다"),
100103
DUPLICATE_POPUP_KEY(HttpStatus.CONFLICT, 40906, "중복된 팝업 키입니다", "중복된 팝업 키입니다"),
101104
ALREADY_DOWNLOADED_THEME(HttpStatus.CONFLICT, 40907, "이미 다운로드한 테마입니다", "이미 다운로드한 테마입니다"),

core/src/main/kotlin/common/exception/SnuttException.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ object InvalidEmailException : SnuttException(ErrorType.INVALID_EMAIL)
3535

3636
object UserEmailIsNotVerifiedException : SnuttException(ErrorType.USER_EMAIL_IS_NOT_VERIFIED)
3737

38+
object InvalidTimetableSemesterException : SnuttException(ErrorType.INVALID_TIMETABLE_SEMESTER)
39+
3840
object LectureNotFoundException : SnuttException(ErrorType.LECTURE_NOT_FOUND)
3941

4042
object UserNotFoundException : SnuttException(ErrorType.USER_NOT_FOUND)
@@ -123,7 +125,7 @@ object TimetableNotFoundException : SnuttException(ErrorType.TIMETABLE_NOT_FOUND
123125

124126
object PrimaryTimetableNotFoundException : SnuttException(ErrorType.PRIMARY_TIMETABLE_NOT_FOUND)
125127

126-
object TimetableNotPrimaryException : SnuttException(ErrorType.DEFAULT_ERROR)
128+
object TimetableNotPrimaryException : SnuttException(ErrorType.TIMETABLE_NOT_PRIMARY)
127129

128130
object ConfigNotFoundException : SnuttException(ErrorType.CONFIG_NOT_FOUND)
129131

@@ -137,6 +139,8 @@ object DiaryQuestionNotFoundException : SnuttException(ErrorType.DIARY_QUESTION_
137139

138140
object DiaryQuestionInvalidException : SnuttException(ErrorType.DIARY_QUESTION_INVALID)
139141

142+
object DiaryCommentTooLongException : SnuttException(ErrorType.DIARY_COMMENT_TOO_LONG)
143+
140144
object DiaryDailyClassTypeNotFoundException : SnuttException(ErrorType.DIARY_DAILY_CLASS_TYPE_NOT_FOUND)
141145

142146
object DiarySubmissionNotFoundException : SnuttException(ErrorType.DIARY_SUBMISSION_NOT_FOUND)
@@ -172,8 +176,6 @@ object DuplicateFriendException : SnuttException(ErrorType.DUPLICATE_FRIEND)
172176

173177
object InvalidFriendException : SnuttException(ErrorType.INVALID_FRIEND)
174178

175-
object DuplicateThemeNameException : SnuttException(ErrorType.DUPLICATE_THEME_NAME)
176-
177179
object InvalidThemeTypeException : SnuttException(ErrorType.INVALID_THEME_TYPE)
178180

179181
object DuplicatePopupKeyException : SnuttException(ErrorType.DUPLICATE_POPUP_KEY)

core/src/main/kotlin/coursebook/repository/CoursebookRepository.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.wafflestudio.snutt.coursebook.repository
22

3+
import com.wafflestudio.snutt.common.enums.Semester
34
import com.wafflestudio.snutt.coursebook.data.Coursebook
45
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
56
import org.springframework.stereotype.Repository
@@ -11,4 +12,9 @@ interface CoursebookRepository : CoroutineCrudRepository<Coursebook, String> {
1112
suspend fun findAllByOrderByYearDescSemesterDesc(): List<Coursebook>
1213

1314
suspend fun findTop3ByOrderByYearDescSemesterDesc(): List<Coursebook>
15+
16+
suspend fun existsByYearAndSemester(
17+
year: Int,
18+
semester: Semester,
19+
): Boolean
1420
}

core/src/main/kotlin/coursebook/service/CoursebookService.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.wafflestudio.snutt.coursebook.service
22

3+
import com.wafflestudio.snutt.common.enums.Semester
34
import com.wafflestudio.snutt.coursebook.data.Coursebook
45
import com.wafflestudio.snutt.coursebook.repository.CoursebookRepository
56
import org.springframework.stereotype.Service
@@ -10,6 +11,11 @@ interface CoursebookService {
1011
suspend fun getCoursebooks(): List<Coursebook>
1112

1213
suspend fun getLastTwoCourseBooksBeforeCurrent(): List<Coursebook>
14+
15+
suspend fun existsCoursebook(
16+
year: Int,
17+
semester: Semester,
18+
): Boolean
1319
}
1420

1521
@Service
@@ -24,4 +30,9 @@ class CoursebookServiceImpl(
2430
coursebookRepository.findTop3ByOrderByYearDescSemesterDesc().slice(
2531
1..2,
2632
)
33+
34+
override suspend fun existsCoursebook(
35+
year: Int,
36+
semester: Semester,
37+
): Boolean = coursebookRepository.existsByYearAndSemester(year, semester)
2738
}

core/src/main/kotlin/diary/service/DiaryService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.wafflestudio.snutt.diary.service
22

33
import com.wafflestudio.snutt.common.enums.Semester
4+
import com.wafflestudio.snutt.common.exception.DiaryCommentTooLongException
45
import com.wafflestudio.snutt.common.exception.DiaryDailyClassTypeNotFoundException
56
import com.wafflestudio.snutt.common.exception.DiaryQuestionInvalidException
67
import com.wafflestudio.snutt.common.exception.DiaryQuestionNotFoundException
@@ -75,6 +76,10 @@ class DiaryServiceImpl(
7576
private val timetableRepository: TimetableRepository,
7677
private val lectureService: LectureService,
7778
) : DiaryService {
79+
companion object {
80+
const val COMMENT_MAX_LENGTH = 1000
81+
}
82+
7883
override suspend fun generateQuestionnaire(
7984
userId: String,
8085
lectureId: String,
@@ -135,6 +140,9 @@ class DiaryServiceImpl(
135140
userId: String,
136141
request: DiarySubmissionRequestDto,
137142
) {
143+
if (request.comment.length > COMMENT_MAX_LENGTH) {
144+
throw DiaryCommentTooLongException
145+
}
138146
val lecture = lectureService.getByIdOrNull(request.lectureId) ?: throw LectureNotFoundException
139147
val dailyClassTypes = diaryDailyClassTypeRepository.findAllByNameIn(request.dailyClassTypes)
140148
val questionIds = request.questionAnswers.map { it.questionId }

core/src/main/kotlin/theme/data/TimetableTheme.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ package com.wafflestudio.snutt.theme.data
22

33
import org.springframework.data.annotation.Id
44
import org.springframework.data.annotation.Transient
5-
import org.springframework.data.mongodb.core.index.CompoundIndex
65
import org.springframework.data.mongodb.core.index.Indexed
76
import org.springframework.data.mongodb.core.mapping.Document
87
import org.springframework.data.mongodb.core.mapping.Field
98
import org.springframework.data.mongodb.core.mapping.FieldType
109
import java.time.LocalDateTime
1110

1211
@Document
13-
@CompoundIndex(def = "{ 'userId': 1, 'name': 1 }", unique = true)
1412
data class TimetableTheme(
1513
@Id
1614
var id: String? = null,

0 commit comments

Comments
 (0)