Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/src/test/kotlin/timetable/TimetableIntegTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 =
Expand Down Expand Up @@ -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<Int>): List<RegistrationStatus> =
private suspend fun getRegistrationStatus(
pages: List<Int>,
coursebook: Coursebook,
): List<RegistrationStatus> =
supervisorScope {
pages
.map { page ->
async {
getSugangSnuSearchContent(page).extractRegistrationStatus()
getSugangSnuSearchContent(page, coursebook).extractRegistrationStatus()
}
}.awaitAll()
.flatten()
Expand Down Expand Up @@ -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(), "")
Expand Down
7 changes: 5 additions & 2 deletions core/src/main/kotlin/common/exception/ErrorType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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가 없습니다", "해당 정보로 가입된 사용자가 없습니다"),
Expand Down Expand Up @@ -72,6 +73,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, "유효하지 않은 애플 로그인 토큰입니다", "소셜 로그인에 실패했습니다"),
Expand All @@ -89,13 +91,14 @@ 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, "이미 친구 관계이거나 친구 요청을 보냈습니다", "이미 친구 관계이거나 친구 요청을 보냈습니다"),
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, "이미 다운로드한 테마입니다", "이미 다운로드한 테마입니다"),
Expand Down
8 changes: 5 additions & 3 deletions core/src/main/kotlin/common/exception/SnuttException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -123,7 +125,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)

Expand All @@ -137,6 +139,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)
Expand Down Expand Up @@ -172,8 +176,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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,4 +12,9 @@ interface CoursebookRepository : CoroutineCrudRepository<Coursebook, String> {
suspend fun findAllByOrderByYearDescSemesterDesc(): List<Coursebook>

suspend fun findTop3ByOrderByYearDescSemesterDesc(): List<Coursebook>

suspend fun existsByYearAndSemester(
year: Int,
semester: Semester,
): Boolean
}
11 changes: 11 additions & 0 deletions core/src/main/kotlin/coursebook/service/CoursebookService.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +11,11 @@ interface CoursebookService {
suspend fun getCoursebooks(): List<Coursebook>

suspend fun getLastTwoCourseBooksBeforeCurrent(): List<Coursebook>

suspend fun existsCoursebook(
year: Int,
semester: Semester,
): Boolean
}

@Service
Expand All @@ -24,4 +30,9 @@ class CoursebookServiceImpl(
coursebookRepository.findTop3ByOrderByYearDescSemesterDesc().slice(
1..2,
)

override suspend fun existsCoursebook(
year: Int,
semester: Semester,
): Boolean = coursebookRepository.existsByYearAndSemester(year, semester)
}
8 changes: 8 additions & 0 deletions core/src/main/kotlin/diary/service/DiaryService.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
Expand Down
2 changes: 0 additions & 2 deletions core/src/main/kotlin/theme/data/TimetableTheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ 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
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,
Expand Down
3 changes: 0 additions & 3 deletions core/src/main/kotlin/theme/service/TimetableThemeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -150,7 +149,6 @@ class TimetableThemeServiceImpl(
colors: List<ColorSet>,
): TimetableTheme {
if (colors.size !in 1..MAX_COLOR_COUNT) throw InvalidThemeColorCountException
if (timetableThemeRepository.existsByUserIdAndName(userId, name)) throw DuplicateThemeNameException

val theme =
TimetableTheme(
Expand All @@ -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
}

Expand Down
7 changes: 5 additions & 2 deletions core/src/main/kotlin/timetables/service/TimetableService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ 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
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
Expand Down Expand Up @@ -288,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
}
Expand Down Expand Up @@ -326,7 +329,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))
}

Expand Down
Loading