From b1c15deb1045abb385f83dd9c0f5aa822277c107 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 4 Apr 2026 20:33:52 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EA=B0=95=EC=9D=98=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=9E=A5=20comment=20=EA=B8=B8=EC=9D=B4=201000=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=9C=ED=95=9C=20(#499)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/kotlin/common/exception/ErrorType.kt | 1 + core/src/main/kotlin/common/exception/SnuttException.kt | 2 ++ core/src/main/kotlin/diary/service/DiaryService.kt | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/core/src/main/kotlin/common/exception/ErrorType.kt b/core/src/main/kotlin/common/exception/ErrorType.kt index 0a15637f..f99c4548 100644 --- a/core/src/main/kotlin/common/exception/ErrorType.kt +++ b/core/src/main/kotlin/common/exception/ErrorType.kt @@ -72,6 +72,7 @@ enum class ErrorType( "현재 일시 중단된 기능입니다. 자세한 내용은 알림을 참고해 주세요.", ), DIARY_QUESTION_INVALID(HttpStatus.BAD_REQUEST, 40026, "강의 일기장 질문이 유효하지 않습니다", "강의 일기장 질문이 유효하지 않습니다"), + DIARY_COMMENT_TOO_LONG(HttpStatus.BAD_REQUEST, 40027, "더 남기고 싶은 말은 최대 1,000자 입력할 수 있습니다", "더 남기고 싶은 말은 최대 1,000자 입력할 수 있습니다"), SOCIAL_CONNECT_FAIL(HttpStatus.UNAUTHORIZED, 40100, "소셜 로그인에 실패했습니다", "소셜 로그인에 실패했습니다"), INVALID_APPLE_LOGIN_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "유효하지 않은 애플 로그인 토큰입니다", "소셜 로그인에 실패했습니다"), diff --git a/core/src/main/kotlin/common/exception/SnuttException.kt b/core/src/main/kotlin/common/exception/SnuttException.kt index e4cc8259..9f8b558e 100644 --- a/core/src/main/kotlin/common/exception/SnuttException.kt +++ b/core/src/main/kotlin/common/exception/SnuttException.kt @@ -137,6 +137,8 @@ object DiaryQuestionNotFoundException : SnuttException(ErrorType.DIARY_QUESTION_ object DiaryQuestionInvalidException : SnuttException(ErrorType.DIARY_QUESTION_INVALID) +object DiaryCommentTooLongException : SnuttException(ErrorType.DIARY_COMMENT_TOO_LONG) + object DiaryDailyClassTypeNotFoundException : SnuttException(ErrorType.DIARY_DAILY_CLASS_TYPE_NOT_FOUND) object DiarySubmissionNotFoundException : SnuttException(ErrorType.DIARY_SUBMISSION_NOT_FOUND) diff --git a/core/src/main/kotlin/diary/service/DiaryService.kt b/core/src/main/kotlin/diary/service/DiaryService.kt index 72e0f131..56107ead 100644 --- a/core/src/main/kotlin/diary/service/DiaryService.kt +++ b/core/src/main/kotlin/diary/service/DiaryService.kt @@ -1,6 +1,7 @@ package com.wafflestudio.snutt.diary.service import com.wafflestudio.snutt.common.enums.Semester +import com.wafflestudio.snutt.common.exception.DiaryCommentTooLongException import com.wafflestudio.snutt.common.exception.DiaryDailyClassTypeNotFoundException import com.wafflestudio.snutt.common.exception.DiaryQuestionInvalidException import com.wafflestudio.snutt.common.exception.DiaryQuestionNotFoundException @@ -75,6 +76,10 @@ class DiaryServiceImpl( private val timetableRepository: TimetableRepository, private val lectureService: LectureService, ) : DiaryService { + companion object { + const val COMMENT_MAX_LENGTH = 1000 + } + override suspend fun generateQuestionnaire( userId: String, lectureId: String, @@ -135,6 +140,9 @@ class DiaryServiceImpl( userId: String, request: DiarySubmissionRequestDto, ) { + if (request.comment.length > COMMENT_MAX_LENGTH) { + throw DiaryCommentTooLongException + } val lecture = lectureService.getByIdOrNull(request.lectureId) ?: throw LectureNotFoundException val dailyClassTypes = diaryDailyClassTypeRepository.findAllByNameIn(request.dailyClassTypes) val questionIds = request.questionAnswers.map { it.questionId } From 80c2075c89cd46d005eca638094726e15dc754e4 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 4 Apr 2026 20:37:29 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=EB=8C=80=ED=91=9C=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=91=9C=20=ED=95=B4=EC=A0=9C=20500=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=20(#502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/kotlin/common/exception/SnuttException.kt | 2 +- core/src/main/kotlin/timetables/service/TimetableService.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/common/exception/SnuttException.kt b/core/src/main/kotlin/common/exception/SnuttException.kt index 9f8b558e..40f9755f 100644 --- a/core/src/main/kotlin/common/exception/SnuttException.kt +++ b/core/src/main/kotlin/common/exception/SnuttException.kt @@ -123,7 +123,7 @@ object TimetableNotFoundException : SnuttException(ErrorType.TIMETABLE_NOT_FOUND object PrimaryTimetableNotFoundException : SnuttException(ErrorType.PRIMARY_TIMETABLE_NOT_FOUND) -object TimetableNotPrimaryException : SnuttException(ErrorType.DEFAULT_ERROR) +object TimetableNotPrimaryException : SnuttException(ErrorType.TIMETABLE_NOT_PRIMARY) object ConfigNotFoundException : SnuttException(ErrorType.CONFIG_NOT_FOUND) diff --git a/core/src/main/kotlin/timetables/service/TimetableService.kt b/core/src/main/kotlin/timetables/service/TimetableService.kt index ca69b95e..eab7e0fe 100644 --- a/core/src/main/kotlin/timetables/service/TimetableService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableService.kt @@ -9,7 +9,6 @@ import com.wafflestudio.snutt.common.exception.InvalidTimetableTitleException import com.wafflestudio.snutt.common.exception.PrimaryTimetableNotFoundException import com.wafflestudio.snutt.common.exception.TableDeleteErrorException import com.wafflestudio.snutt.common.exception.TimetableNotFoundException -import com.wafflestudio.snutt.common.exception.TimetableNotPrimaryException import com.wafflestudio.snutt.coursebook.data.CoursebookDto import com.wafflestudio.snutt.coursebook.service.CoursebookService import com.wafflestudio.snutt.evaluation.service.EvService @@ -326,7 +325,7 @@ class TimetableServiceImpl( timetableId: String, ) { val table = timetableRepository.findById(timetableId) ?: throw TimetableNotFoundException - if (table.isPrimary != true) throw TimetableNotPrimaryException + if (table.isPrimary != true) return timetableRepository.save(table.copy(isPrimary = false)) } From 61f9147774b55b9247e0a30aa3c9572931239685 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 11 Apr 2026 15:32:24 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=ED=85=8C=EB=A7=88=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=97=88=EC=9A=A9=20(#503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/kotlin/common/exception/ErrorType.kt | 3 ++- core/src/main/kotlin/common/exception/SnuttException.kt | 2 -- core/src/main/kotlin/theme/data/TimetableTheme.kt | 2 -- core/src/main/kotlin/theme/service/TimetableThemeService.kt | 3 --- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/core/src/main/kotlin/common/exception/ErrorType.kt b/core/src/main/kotlin/common/exception/ErrorType.kt index f99c4548..f2bc5f18 100644 --- a/core/src/main/kotlin/common/exception/ErrorType.kt +++ b/core/src/main/kotlin/common/exception/ErrorType.kt @@ -96,7 +96,8 @@ enum class ErrorType( DUPLICATE_EMAIL(HttpStatus.CONFLICT, 40901, "이미 사용 중인 이메일입니다", "이미 사용 중인 이메일입니다", "회원가입 실패"), DUPLICATE_FRIEND(HttpStatus.CONFLICT, 40902, "이미 친구 관계이거나 친구 요청을 보냈습니다", "이미 친구 관계이거나 친구 요청을 보냈습니다"), INVALID_FRIEND(HttpStatus.CONFLICT, 40903, "친구 요청을 보낼 수 없는 유저입니다", "친구 요청을 보낼 수 없는 유저입니다"), - DUPLICATE_THEME_NAME(HttpStatus.CONFLICT, 40904, "중복된 테마 이름입니다", "동일한 테마 이름이 이미 있습니다. 다른 이름을 입력해 주세요", "테마 이름 중복"), + + // DUPLICATE_THEME_NAME(HttpStatus.CONFLICT, 40904, "중복된 테마 이름입니다", "동일한 테마 이름이 이미 있습니다. 다른 이름을 입력해 주세요", "테마 이름 중복"), INVALID_THEME_TYPE(HttpStatus.CONFLICT, 40905, "적절하지 않은 유형의 테마입니다", "적절하지 않은 유형의 테마입니다"), DUPLICATE_POPUP_KEY(HttpStatus.CONFLICT, 40906, "중복된 팝업 키입니다", "중복된 팝업 키입니다"), ALREADY_DOWNLOADED_THEME(HttpStatus.CONFLICT, 40907, "이미 다운로드한 테마입니다", "이미 다운로드한 테마입니다"), diff --git a/core/src/main/kotlin/common/exception/SnuttException.kt b/core/src/main/kotlin/common/exception/SnuttException.kt index 40f9755f..375bc3ef 100644 --- a/core/src/main/kotlin/common/exception/SnuttException.kt +++ b/core/src/main/kotlin/common/exception/SnuttException.kt @@ -174,8 +174,6 @@ object DuplicateFriendException : SnuttException(ErrorType.DUPLICATE_FRIEND) object InvalidFriendException : SnuttException(ErrorType.INVALID_FRIEND) -object DuplicateThemeNameException : SnuttException(ErrorType.DUPLICATE_THEME_NAME) - object InvalidThemeTypeException : SnuttException(ErrorType.INVALID_THEME_TYPE) object DuplicatePopupKeyException : SnuttException(ErrorType.DUPLICATE_POPUP_KEY) diff --git a/core/src/main/kotlin/theme/data/TimetableTheme.kt b/core/src/main/kotlin/theme/data/TimetableTheme.kt index cee13636..1466ce3b 100644 --- a/core/src/main/kotlin/theme/data/TimetableTheme.kt +++ b/core/src/main/kotlin/theme/data/TimetableTheme.kt @@ -2,7 +2,6 @@ package com.wafflestudio.snutt.theme.data import org.springframework.data.annotation.Id import org.springframework.data.annotation.Transient -import org.springframework.data.mongodb.core.index.CompoundIndex import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Field @@ -10,7 +9,6 @@ import org.springframework.data.mongodb.core.mapping.FieldType import java.time.LocalDateTime @Document -@CompoundIndex(def = "{ 'userId': 1, 'name': 1 }", unique = true) data class TimetableTheme( @Id var id: String? = null, diff --git a/core/src/main/kotlin/theme/service/TimetableThemeService.kt b/core/src/main/kotlin/theme/service/TimetableThemeService.kt index 69931fc6..398a245a 100644 --- a/core/src/main/kotlin/theme/service/TimetableThemeService.kt +++ b/core/src/main/kotlin/theme/service/TimetableThemeService.kt @@ -2,7 +2,6 @@ package com.wafflestudio.snutt.theme.service import com.wafflestudio.snutt.common.enums.BasicThemeType import com.wafflestudio.snutt.common.exception.AlreadyDownloadedThemeException -import com.wafflestudio.snutt.common.exception.DuplicateThemeNameException import com.wafflestudio.snutt.common.exception.InvalidThemeColorCountException import com.wafflestudio.snutt.common.exception.InvalidThemeTypeException import com.wafflestudio.snutt.common.exception.NotDefaultThemeErrorException @@ -150,7 +149,6 @@ class TimetableThemeServiceImpl( colors: List, ): TimetableTheme { if (colors.size !in 1..MAX_COLOR_COUNT) throw InvalidThemeColorCountException - if (timetableThemeRepository.existsByUserIdAndName(userId, name)) throw DuplicateThemeNameException val theme = TimetableTheme( @@ -171,7 +169,6 @@ class TimetableThemeServiceImpl( val theme = getCustomTheme(userId, themeId) name?.let { - if (theme.name != it && timetableThemeRepository.existsByUserIdAndName(userId, it)) throw DuplicateThemeNameException theme.name = it } From ab514a43335266d84781dcb86e272955d706b54d Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Tue, 28 Apr 2026 15:52:34 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20coursebook=EC=9D=B4=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=ED=95=99=EA=B8=B0=EC=9D=B8=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20(#507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/test/kotlin/timetable/TimetableIntegTest.kt | 3 +++ core/src/main/kotlin/common/exception/ErrorType.kt | 1 + .../main/kotlin/common/exception/SnuttException.kt | 2 ++ .../coursebook/repository/CoursebookRepository.kt | 6 ++++++ .../kotlin/coursebook/service/CoursebookService.kt | 11 +++++++++++ .../kotlin/timetables/service/TimetableService.kt | 4 ++++ 6 files changed, 27 insertions(+) diff --git a/api/src/test/kotlin/timetable/TimetableIntegTest.kt b/api/src/test/kotlin/timetable/TimetableIntegTest.kt index 8760b022..23c91673 100644 --- a/api/src/test/kotlin/timetable/TimetableIntegTest.kt +++ b/api/src/test/kotlin/timetable/TimetableIntegTest.kt @@ -3,6 +3,7 @@ package com.wafflestudio.snutt.timetable import BaseIntegTest import com.ninjasquad.springmockk.MockkBean import com.wafflestudio.snutt.config.USER_ATTRIBUTE_KEY +import com.wafflestudio.snutt.coursebook.service.CoursebookService import com.wafflestudio.snutt.evaluation.service.EvService import com.wafflestudio.snutt.filter.ApiKeyWebFilter import com.wafflestudio.snutt.filter.UserAuthenticationWebFilter @@ -29,6 +30,7 @@ import timetables.dto.TimetableBriefDto @AutoConfigureWebTestClient class TimetableIntegTest( @MockkBean private val mockSnuttEvService: EvService, + @MockkBean private val mockCoursebookService: CoursebookService, @MockkBean private val apiKeyWebFilter: ApiKeyWebFilter, @MockkBean private val userAuthenticationWebFilter: UserAuthenticationWebFilter, val webTestClient: WebTestClient, @@ -39,6 +41,7 @@ class TimetableIntegTest( ) : BaseIntegTest({ coEvery { mockSnuttEvService.getSummariesByIds(any()) } returns emptyList() coEvery { mockSnuttEvService.getEvIdsBySnuttIds(any()) } returns emptyList() + coEvery { mockCoursebookService.existsCoursebook(any(), any()) } returns true coEvery { apiKeyWebFilter.filter(any(), any()) } answers { diff --git a/core/src/main/kotlin/common/exception/ErrorType.kt b/core/src/main/kotlin/common/exception/ErrorType.kt index f2bc5f18..67c466fd 100644 --- a/core/src/main/kotlin/common/exception/ErrorType.kt +++ b/core/src/main/kotlin/common/exception/ErrorType.kt @@ -31,6 +31,7 @@ enum class ErrorType( CANNOT_RESET_CUSTOM_LECTURE(HttpStatus.FORBIDDEN, 0x300D, "cannot reset custom lectures", "직접 만든 강좌는 초기화할 수 없습니다"), INVALID_EMAIL(HttpStatus.FORBIDDEN, 0x300F, "email이 유효하지 않습니다", "이메일 형식이 올바르지 않습니다. 다시 입력해 주세요"), USER_EMAIL_IS_NOT_VERIFIED(HttpStatus.FORBIDDEN, 0x3011, "User email is not verified", "이메일 인증이 완료된 후 강의평을 확인할 수 있습니다", "이메일 인증 필요"), + INVALID_TIMETABLE_SEMESTER(HttpStatus.FORBIDDEN, 0x300B, "시간표를 생성할 수 없는 학기입니다", "시간표를 생성할 수 없는 학기입니다"), LECTURE_NOT_FOUND(HttpStatus.NOT_FOUND, 0x4003, "lecture가 없습니다", "수강편람에서 찾을 수 없는 강좌입니다"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, 0x4004, "user가 없습니다", "해당 정보로 가입된 사용자가 없습니다"), diff --git a/core/src/main/kotlin/common/exception/SnuttException.kt b/core/src/main/kotlin/common/exception/SnuttException.kt index 375bc3ef..23bf195a 100644 --- a/core/src/main/kotlin/common/exception/SnuttException.kt +++ b/core/src/main/kotlin/common/exception/SnuttException.kt @@ -35,6 +35,8 @@ object InvalidEmailException : SnuttException(ErrorType.INVALID_EMAIL) object UserEmailIsNotVerifiedException : SnuttException(ErrorType.USER_EMAIL_IS_NOT_VERIFIED) +object InvalidTimetableSemesterException : SnuttException(ErrorType.INVALID_TIMETABLE_SEMESTER) + object LectureNotFoundException : SnuttException(ErrorType.LECTURE_NOT_FOUND) object UserNotFoundException : SnuttException(ErrorType.USER_NOT_FOUND) diff --git a/core/src/main/kotlin/coursebook/repository/CoursebookRepository.kt b/core/src/main/kotlin/coursebook/repository/CoursebookRepository.kt index bdc07e11..2b0a3733 100644 --- a/core/src/main/kotlin/coursebook/repository/CoursebookRepository.kt +++ b/core/src/main/kotlin/coursebook/repository/CoursebookRepository.kt @@ -1,5 +1,6 @@ package com.wafflestudio.snutt.coursebook.repository +import com.wafflestudio.snutt.common.enums.Semester import com.wafflestudio.snutt.coursebook.data.Coursebook import org.springframework.data.repository.kotlin.CoroutineCrudRepository import org.springframework.stereotype.Repository @@ -11,4 +12,9 @@ interface CoursebookRepository : CoroutineCrudRepository { suspend fun findAllByOrderByYearDescSemesterDesc(): List suspend fun findTop3ByOrderByYearDescSemesterDesc(): List + + suspend fun existsByYearAndSemester( + year: Int, + semester: Semester, + ): Boolean } diff --git a/core/src/main/kotlin/coursebook/service/CoursebookService.kt b/core/src/main/kotlin/coursebook/service/CoursebookService.kt index dba26740..c689477d 100644 --- a/core/src/main/kotlin/coursebook/service/CoursebookService.kt +++ b/core/src/main/kotlin/coursebook/service/CoursebookService.kt @@ -1,5 +1,6 @@ package com.wafflestudio.snutt.coursebook.service +import com.wafflestudio.snutt.common.enums.Semester import com.wafflestudio.snutt.coursebook.data.Coursebook import com.wafflestudio.snutt.coursebook.repository.CoursebookRepository import org.springframework.stereotype.Service @@ -10,6 +11,11 @@ interface CoursebookService { suspend fun getCoursebooks(): List suspend fun getLastTwoCourseBooksBeforeCurrent(): List + + suspend fun existsCoursebook( + year: Int, + semester: Semester, + ): Boolean } @Service @@ -24,4 +30,9 @@ class CoursebookServiceImpl( coursebookRepository.findTop3ByOrderByYearDescSemesterDesc().slice( 1..2, ) + + override suspend fun existsCoursebook( + year: Int, + semester: Semester, + ): Boolean = coursebookRepository.existsByYearAndSemester(year, semester) } diff --git a/core/src/main/kotlin/timetables/service/TimetableService.kt b/core/src/main/kotlin/timetables/service/TimetableService.kt index eab7e0fe..47d06d63 100644 --- a/core/src/main/kotlin/timetables/service/TimetableService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableService.kt @@ -5,6 +5,7 @@ import com.wafflestudio.snutt.common.dynamiclink.dto.DynamicLinkResponse import com.wafflestudio.snutt.common.enums.BasicThemeType import com.wafflestudio.snutt.common.enums.Semester import com.wafflestudio.snutt.common.exception.DuplicateTimetableTitleException +import com.wafflestudio.snutt.common.exception.InvalidTimetableSemesterException import com.wafflestudio.snutt.common.exception.InvalidTimetableTitleException import com.wafflestudio.snutt.common.exception.PrimaryTimetableNotFoundException import com.wafflestudio.snutt.common.exception.TableDeleteErrorException @@ -287,6 +288,9 @@ class TimetableServiceImpl( if (title.isEmpty()) { throw InvalidTimetableTitleException } + if (!coursebookService.existsCoursebook(year, semester)) { + throw InvalidTimetableSemesterException + } if (timetableRepository.existsByUserIdAndYearAndSemesterAndTitle(userId, year, semester, title)) { throw DuplicateTimetableTitleException } From 5e00e05b562054d70d415bc12ed69702ae336d6d Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 29 Apr 2026 09:39:44 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=EC=98=88=EB=B9=84=EC=88=98=EA=B0=95?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=9D=BC=EC=A0=95=EC=97=90=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9D=8C=20(#509)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sugangsnu/common/utils/RegistrationPeriodParseUtils.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/batch/src/main/kotlin/sugangsnu/common/utils/RegistrationPeriodParseUtils.kt b/batch/src/main/kotlin/sugangsnu/common/utils/RegistrationPeriodParseUtils.kt index 187b57bf..8a6a08dd 100644 --- a/batch/src/main/kotlin/sugangsnu/common/utils/RegistrationPeriodParseUtils.kt +++ b/batch/src/main/kotlin/sugangsnu/common/utils/RegistrationPeriodParseUtils.kt @@ -28,6 +28,7 @@ object RegistrationPeriodParseUtils { private fun parseRegistrationPhase(typeText: String): RegistrationPhase? = when { + typeText.contains("예비") -> null typeText.contains("전산확정") -> null typeText.contains("정원외") -> null typeText.contains("수강취소") -> null From b8fd12b7afee92604fd909d41bb9413eeefacb03 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 29 Apr 2026 09:39:56 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EA=B0=95=EC=9D=98=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=9E=A5=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=88=98=EC=A0=95=20(#505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/kotlin/common/exception/ErrorType.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/common/exception/ErrorType.kt b/core/src/main/kotlin/common/exception/ErrorType.kt index 67c466fd..7c71c755 100644 --- a/core/src/main/kotlin/common/exception/ErrorType.kt +++ b/core/src/main/kotlin/common/exception/ErrorType.kt @@ -91,8 +91,8 @@ enum class ErrorType( SOCIAL_PROVIDER_NOT_ATTACHED(HttpStatus.NOT_FOUND, 40410, "소셜 계정이 연동되지 않았습니다", "소셜 계정이 연동되지 않았습니다"), DIARY_QUESTION_NOT_FOUND(HttpStatus.NOT_FOUND, 40411, "강의 일기장 질문이 유효하지 않습니다", "강의 일기장 질문이 유효하지 않습니다"), DIARY_DAILY_CLASS_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, 40412, "강의 일기장 오늘 한 일이 유효하지 않습니다", "강의 일기장 오늘 한 일이 유효하지 않습니다"), - DIARY_SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, 40412, "강의 일기장 기록이 유효하지 않습니다", "강의 일기장 기록이 유효하지 않습니다"), DIARY_TARGET_LECTURE_NOT_FOUND(HttpStatus.NOT_FOUND, 40413, "강의 일기장을 작성할 강의가 없습니다", "강의 일기장을 작성할 강의가 없습니다"), + DIARY_SUBMISSION_NOT_FOUND(HttpStatus.NOT_FOUND, 40414, "강의 일기장 기록이 유효하지 않습니다", "강의 일기장 기록이 유효하지 않습니다"), DUPLICATE_VACANCY_NOTIFICATION(HttpStatus.CONFLICT, 40900, "빈자리 알림 중복", "이미 빈자리 알림을 받고 있는 강좌입니다"), DUPLICATE_EMAIL(HttpStatus.CONFLICT, 40901, "이미 사용 중인 이메일입니다", "이미 사용 중인 이메일입니다", "회원가입 실패"), DUPLICATE_FRIEND(HttpStatus.CONFLICT, 40902, "이미 친구 관계이거나 친구 요청을 보냈습니다", "이미 친구 관계이거나 친구 요청을 보냈습니다"), From 96aa3d1556217d04c235524df6f2baaf0f288f02 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 29 Apr 2026 10:13:01 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EB=B9=88=EC=9E=90=EB=A6=AC=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EA=B2=80=EC=83=89=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=ED=95=99=EA=B8=B0=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=20(#510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sugangsnu/common/SugangSnuRepository.kt | 8 +++++- .../service/VacancyNotifierService.kt | 27 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt b/batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt index a521d8e8..93974a96 100644 --- a/batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt +++ b/batch/src/main/kotlin/sugangsnu/common/SugangSnuRepository.kt @@ -51,13 +51,19 @@ class SugangSnuRepository( """.trimIndent().replace("\n", "") } - suspend fun getSearchPageHtml(pageNo: Int = 1): PooledDataBuffer = + suspend fun getSearchPageHtml( + year: Int, + semester: Semester, + pageNo: Int = 1, + ): PooledDataBuffer = sugangSnuApi .get() .uri { builder -> builder .path(SUGANG_SNU_SEARCH_PATH) .query(DEFAULT_SEARCH_PAGE_PARAMS) + .queryParam("srchOpenSchyy", year) + .queryParam("srchOpenShtm", convertSemesterToSugangSnuSearchString(semester)) .queryParam("pageNo", pageNo) .build() }.accept(MediaType.TEXT_HTML) diff --git a/batch/src/main/kotlin/sugangsnu/job/vacancynotification/service/VacancyNotifierService.kt b/batch/src/main/kotlin/sugangsnu/job/vacancynotification/service/VacancyNotifierService.kt index 0daaa164..fe463f8b 100644 --- a/batch/src/main/kotlin/sugangsnu/job/vacancynotification/service/VacancyNotifierService.kt +++ b/batch/src/main/kotlin/sugangsnu/job/vacancynotification/service/VacancyNotifierService.kt @@ -65,7 +65,7 @@ class VacancyNotifierServiceImpl( log.info("시작: ${registrationPhase.name}") val pageCount = runCatching { - getPageCount() + getPageCount(coursebook) }.getOrElse { log.error("에러가 발생했거나 부하 기간입니다. {}", it.message, it) delay(30L.seconds) @@ -77,7 +77,7 @@ class VacancyNotifierServiceImpl( // 수강 사이트 부하를 분산하기 위해 강의 전체를 20등분해서 각각 요청 (1..pageCount).chunked(pageCount / 20).forEach { chunkedPages -> - val registrationStatus = getRegistrationStatus(chunkedPages) + val registrationStatus = getRegistrationStatus(chunkedPages, coursebook) if (registrationStatus.all { it.registrationCount == 0 }) return VacancyNotificationJobResult.REGISTRATION_IS_NOT_STARTED val registrationStatusMap = @@ -157,19 +157,22 @@ class VacancyNotifierServiceImpl( this.quota == this.registrationCount } - private suspend fun getPageCount(): Int { - val firstPageContent = getSugangSnuSearchContent(1) + private suspend fun getPageCount(coursebook: Coursebook): Int { + val firstPageContent = getSugangSnuSearchContent(1, coursebook) val totalCount = firstPageContent.select("div.content > div.search-result-con > small > em").text().toInt() return (totalCount + 9) / COUNT_PER_PAGE } - private suspend fun getRegistrationStatus(pages: List): List = + private suspend fun getRegistrationStatus( + pages: List, + coursebook: Coursebook, + ): List = supervisorScope { pages .map { page -> async { - getSugangSnuSearchContent(page).extractRegistrationStatus() + getSugangSnuSearchContent(page, coursebook).extractRegistrationStatus() } }.awaitAll() .flatten() @@ -211,8 +214,16 @@ class VacancyNotifierServiceImpl( } } - private suspend fun getSugangSnuSearchContent(pageNo: Int): Element { - val webPageDataBuffer = sugangSnuRepository.getSearchPageHtml(pageNo) + private suspend fun getSugangSnuSearchContent( + pageNo: Int, + coursebook: Coursebook, + ): Element { + val webPageDataBuffer = + sugangSnuRepository.getSearchPageHtml( + year = coursebook.year, + semester = coursebook.semester, + pageNo = pageNo, + ) return try { Jsoup .parse(webPageDataBuffer.asInputStream(), Charsets.UTF_8.name(), "")