From 84dd46df33a0425a10ce311a7d7365c8babae16f Mon Sep 17 00:00:00 2001 From: Hankyeol Choi Date: Sun, 7 Jan 2024 02:03:37 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=B6=94=EA=B0=80,=EC=82=AD=EC=A0=9C,=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=8B=9C=20=EC=9D=91=EB=8B=B5=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/handler/TimetableLectureHandler.kt | 12 +++--- .../service/TimetableLectureService.kt | 40 ++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/api/src/main/kotlin/handler/TimetableLectureHandler.kt b/api/src/main/kotlin/handler/TimetableLectureHandler.kt index 7734eff5..07e8296e 100644 --- a/api/src/main/kotlin/handler/TimetableLectureHandler.kt +++ b/api/src/main/kotlin/handler/TimetableLectureHandler.kt @@ -2,6 +2,7 @@ package com.wafflestudio.snu4t.handler import com.fasterxml.jackson.annotation.JsonProperty import com.wafflestudio.snu4t.middleware.SnuttRestApiDefaultMiddleware +import com.wafflestudio.snu4t.timetables.dto.TimetableLegacyDto import com.wafflestudio.snu4t.timetables.dto.request.CustomTimetableLectureAddLegacyRequestDto import com.wafflestudio.snu4t.timetables.dto.request.TimetableLectureModifyLegacyRequestDto import com.wafflestudio.snu4t.timetables.service.TimetableLectureService @@ -28,7 +29,7 @@ class TimetableLectureHandler( timetableId = timetableId, timetableLectureRequest = customTimetable, isForced = isForced, - ) + ).let(::TimetableLegacyDto) } suspend fun addLecture(req: ServerRequest): ServerResponse = handle(req) { @@ -42,7 +43,7 @@ class TimetableLectureHandler( timetableId = timetableId, lectureId = lectureId, isForced = isForced, - ) + ).let(::TimetableLegacyDto) } suspend fun resetTimetableLecture(req: ServerRequest): ServerResponse = handle(req) { @@ -54,7 +55,7 @@ class TimetableLectureHandler( userId = userId, timetableId = timetableId, timetableLectureId = timetableLectureId, - ) + ).let(::TimetableLegacyDto) } suspend fun modifyTimetableLecture(req: ServerRequest): ServerResponse = handle(req) { @@ -67,9 +68,10 @@ class TimetableLectureHandler( timetableLectureService.modifyTimetableLecture( userId = userId, timetableId = timetableId, + timetableLectureId = timetableLectureId, modifyTimetableLectureRequestDto = modifyRequestDto, isForced = isForced, - ) + ).let(::TimetableLegacyDto) } suspend fun deleteTimetableLecture(req: ServerRequest): ServerResponse = handle(req) { @@ -81,7 +83,7 @@ class TimetableLectureHandler( userId = userId, timetableId = timetableId, timetableLectureId = timetableLectureId, - ) + ).let(::TimetableLegacyDto) } data class ForcedReq( diff --git a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt index 4aefbb0b..f55dcedf 100644 --- a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt @@ -17,23 +17,24 @@ import com.wafflestudio.snu4t.timetables.utils.ColorUtils import org.springframework.stereotype.Service interface TimetableLectureService { - suspend fun addLecture(userId: String, timetableId: String, lectureId: String, isForced: Boolean) + suspend fun addLecture(userId: String, timetableId: String, lectureId: String, isForced: Boolean): Timetable suspend fun addCustomTimetableLecture( userId: String, timetableId: String, timetableLectureRequest: CustomTimetableLectureAddLegacyRequestDto, isForced: Boolean - ) + ): Timetable - suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String) + suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable suspend fun modifyTimetableLecture( userId: String, timetableId: String, + timetableLectureId: String, modifyTimetableLectureRequestDto: TimetableLectureModifyLegacyRequestDto, isForced: Boolean - ) + ): Timetable - suspend fun deleteTimetableLecture(userId: String, timetableId: String, timetableLectureId: String) + suspend fun deleteTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable } @Service @@ -41,7 +42,7 @@ class TimetableLectureServiceImpl( private val timetableRepository: TimetableRepository, private val lectureRepository: LectureRepository, ) : TimetableLectureService { - override suspend fun addLecture(userId: String, timetableId: String, lectureId: String, isForced: Boolean) { + override suspend fun addLecture(userId: String, timetableId: String, lectureId: String, isForced: Boolean): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val lecture = lectureRepository.findById(lectureId) ?: throw LectureNotFoundException if (!(timetable.year == lecture.year && timetable.semester == lecture.semester)) { @@ -49,7 +50,7 @@ class TimetableLectureServiceImpl( } val colorIndex = ColorUtils.getLeastUsedColorIndexByRandom(timetable.lectures.map { it.colorIndex }) if (timetable.lectures.any { it.lectureId == lectureId }) throw DuplicateTimetableLectureException - addTimetableLecture(timetable, TimetableLecture(lecture, colorIndex), isForced) + return addTimetableLecture(timetable, TimetableLecture(lecture, colorIndex), isForced) } override suspend fun addCustomTimetableLecture( @@ -57,13 +58,13 @@ class TimetableLectureServiceImpl( timetableId: String, timetableLectureRequest: CustomTimetableLectureAddLegacyRequestDto, isForced: Boolean - ) { + ): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetableLectureRequest.toTimetableLecture() - addTimetableLecture(timetable, timetableLecture, isForced) + return addTimetableLecture(timetable, timetableLecture, isForced) } - override suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String) { + override suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetable.lectures.find { it.id == timetableLectureId } ?: throw LectureNotFoundException val originalLectureId = timetableLecture.lectureId ?: throw CustomLectureResetException @@ -77,17 +78,18 @@ class TimetableLectureServiceImpl( remark = originalLecture.remark classPlaceAndTimes = originalLecture.classPlaceAndTimes } - timetableRepository.updateLecture(timetableId, timetableLecture) + return timetableRepository.updateLecture(timetableId, timetableLecture) } override suspend fun modifyTimetableLecture( userId: String, timetableId: String, + timetableLectureId: String, modifyTimetableLectureRequestDto: TimetableLectureModifyLegacyRequestDto, isForced: Boolean - ) { + ): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException - val timetableLecture = timetable.lectures.find { it.id == modifyTimetableLectureRequestDto.id } ?: throw LectureNotFoundException + val timetableLecture = timetable.lectures.find { it.id == timetableLectureId } ?: throw LectureNotFoundException timetableLecture.apply { courseTitle = modifyTimetableLectureRequestDto.courseTitle instructor = modifyTimetableLectureRequestDto.instructor ?: instructor @@ -97,19 +99,19 @@ class TimetableLectureServiceImpl( colorIndex = modifyTimetableLectureRequestDto.colorIndex ?: colorIndex classPlaceAndTimes = modifyTimetableLectureRequestDto.classPlaceAndTimes?.map { it.toClassPlaceAndTime() } ?: classPlaceAndTimes } - timetableRepository.updateLecture(timetableId, timetableLecture) + return timetableRepository.updateLecture(timetableId, timetableLecture) } - override suspend fun deleteTimetableLecture(userId: String, timetableId: String, timetableLectureId: String) { + override suspend fun deleteTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable { timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException - timetableRepository.pullLecture(timetableId, timetableLectureId) + return timetableRepository.pullLecture(timetableId, timetableLectureId) } private suspend fun addTimetableLecture( timetable: Timetable, timetableLecture: TimetableLecture, isForced: Boolean - ) { + ): Timetable { val overlappingLectures = timetable.lectures.filter { timetableLecture.id != it.id && ClassTimeUtils.timesOverlap( @@ -126,12 +128,12 @@ class TimetableLectureServiceImpl( timetableRepository.pullLectures(timetable.id!!, overlappingLectures.map { it.id!! }) } } - timetableRepository.pushLecture(timetable.id!!, timetableLecture) + return timetableRepository.pushLecture(timetable.id!!, timetableLecture) } private fun makeOverwritingConfirmMessage(overlappingLectures: List): String { val overlappingLectureTitles = - overlappingLectures.map { "'${it.courseTitle}'" }.subList(0, 2).joinToString(", ") + overlappingLectures.map { "'${it.courseTitle}'" }.take(2).joinToString(", ") val shortFormOfTitles = if (overlappingLectures.size < 3) "" else "외 ${overlappingLectures.size - 2}개의 " return "$overlappingLectureTitles ${shortFormOfTitles}강의가 중복되어 있습니다. 강의를 덮어쓰시겠습니까?" } From e05c649766652ee0a625248cf662f423214b432a Mon Sep 17 00:00:00 2001 From: Hankyeol Choi Date: Sun, 7 Jan 2024 04:05:33 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=B6=94=EA=B0=80,=EC=82=AD=EC=A0=9C,=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20api=20=EB=B2=84=EA=B7=B8=ED=94=BD=EC=8A=A4=20(#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/handler/TimetableLectureHandler.kt | 5 +- .../timetables/data/TimetableLecture.kt | 4 +- .../TimetableLectureModifyLegacyRequestDto.kt | 9 ++-- .../repository/TimetableCustomRepository.kt | 48 ++++++++----------- .../service/TimetableLectureService.kt | 7 ++- .../src/main/resources/application-common.yml | 2 +- 6 files changed, 38 insertions(+), 37 deletions(-) diff --git a/api/src/main/kotlin/handler/TimetableLectureHandler.kt b/api/src/main/kotlin/handler/TimetableLectureHandler.kt index 07e8296e..851297c4 100644 --- a/api/src/main/kotlin/handler/TimetableLectureHandler.kt +++ b/api/src/main/kotlin/handler/TimetableLectureHandler.kt @@ -10,6 +10,7 @@ import org.springframework.stereotype.Component import org.springframework.web.reactive.function.server.ServerRequest import org.springframework.web.reactive.function.server.ServerResponse import org.springframework.web.reactive.function.server.awaitBody +import org.springframework.web.reactive.function.server.awaitBodyOrNull @Component class TimetableLectureHandler( @@ -36,7 +37,7 @@ class TimetableLectureHandler( val userId = req.userId val timetableId = req.pathVariable("timetableId") val lectureId = req.pathVariable("lectureId") - val isForced = req.awaitBody().isForced + val isForced = req.awaitBodyOrNull()?.isForced ?: false timetableLectureService.addLecture( userId = userId, @@ -88,6 +89,6 @@ class TimetableLectureHandler( data class ForcedReq( @JsonProperty("is_forced") - val isForced: Boolean + val isForced: Boolean? ) } diff --git a/core/src/main/kotlin/timetables/data/TimetableLecture.kt b/core/src/main/kotlin/timetables/data/TimetableLecture.kt index a0850af5..5c59d6f6 100644 --- a/core/src/main/kotlin/timetables/data/TimetableLecture.kt +++ b/core/src/main/kotlin/timetables/data/TimetableLecture.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import com.wafflestudio.snu4t.lectures.data.ClassPlaceAndTime import com.wafflestudio.snu4t.lectures.data.Lecture +import org.bson.types.ObjectId import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Field @@ -14,7 +15,7 @@ import org.springframework.data.mongodb.core.mapping.FieldType data class TimetableLecture( @Id @JsonProperty("_id") - var id: String? = null, + var id: String = ObjectId.get().toHexString(), @Field("academic_year") @JsonProperty("academic_year") var academicYear: String?, @@ -45,7 +46,6 @@ data class TimetableLecture( ) fun TimetableLecture(lecture: Lecture, colorIndex: Int) = TimetableLecture( - id = null, lectureId = lecture.id, academicYear = lecture.academicYear, category = lecture.category, diff --git a/core/src/main/kotlin/timetables/dto/request/TimetableLectureModifyLegacyRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/TimetableLectureModifyLegacyRequestDto.kt index 28eba211..cabfc593 100644 --- a/core/src/main/kotlin/timetables/dto/request/TimetableLectureModifyLegacyRequestDto.kt +++ b/core/src/main/kotlin/timetables/dto/request/TimetableLectureModifyLegacyRequestDto.kt @@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.wafflestudio.snu4t.timetables.data.ColorSet data class TimetableLectureModifyLegacyRequestDto( - @JsonProperty("_id") - val id: String, @JsonProperty("course_title") - val courseTitle: String, + val courseTitle: String?, + @JsonProperty("academic_year") + val academicYear: String?, + val category: String?, + val classification: String?, val instructor: String?, val credit: Long?, @JsonProperty("class_time_json") @@ -15,5 +17,6 @@ data class TimetableLectureModifyLegacyRequestDto( val remark: String?, val color: ColorSet?, val colorIndex: Int?, + @JsonProperty("is_forced") val isForced: Boolean = false, ) diff --git a/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt b/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt index 64541aad..01f7c040 100644 --- a/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt +++ b/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt @@ -10,7 +10,9 @@ import com.wafflestudio.snu4t.timetables.data.TimetableLecture import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.bson.types.ObjectId import org.springframework.data.mapping.toDotPath +import org.springframework.data.mongodb.core.FindAndModifyOptions import org.springframework.data.mongodb.core.ReactiveMongoTemplate import org.springframework.data.mongodb.core.find import org.springframework.data.mongodb.core.findModifyAndAwait @@ -29,16 +31,11 @@ interface TimetableCustomRepository { lectureNumber: String ): Flow - suspend fun pushLecture(timeTableId: String, lecture: TimetableLecture): Timetable - suspend fun pullLecture(timeTableId: String, lectureId: String): Timetable - suspend fun pullLectures(timeTableId: String, lectureIds: List): Timetable + suspend fun pushLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable + suspend fun pullLecture(timeTableId: String, timetableLectureId: String): Timetable + suspend fun pullLectures(timeTableId: String, timetableLectureIds: List): Timetable suspend fun updateLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable - suspend fun findLatestChildTimetable( - userId: String, - year: Int, - semester: Semester, - title: String - ): Timetable? + suspend fun findLatestChildTimetable(userId: String, year: Int, semester: Semester, title: String): Timetable? } class TimetableCustomRepositoryImpl( @@ -72,35 +69,32 @@ class TimetableCustomRepositoryImpl( ).asFlow() } - override suspend fun pushLecture(timeTableId: String, lecture: TimetableLecture): Timetable = + override suspend fun pushLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( - Update().push(Timetable::lectures.toDotPath(), lecture).currentDate(Timetable::updatedAt.toDotPath()), - ).findModifyAndAwait() - - override suspend fun pullLecture(timeTableId: String, lectureId: String): Timetable = + Update().push(Timetable::lectures.toDotPath(), timetableLecture) + .currentDate(Timetable::updatedAt.toDotPath()), + ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() + override suspend fun pullLecture(timeTableId: String, timetableLectureId: String): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( Update().pull( Timetable::lectures.toDotPath(), - Query.query(TimetableLecture::lectureId isEqualTo lectureId) + Query.query(TimetableLecture::id isEqualTo timetableLectureId) ).currentDate(Timetable::updatedAt.toDotPath()), - ).findModifyAndAwait() + ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() - override suspend fun pullLectures(timeTableId: String, lectureIds: List): Timetable = + override suspend fun pullLectures(timeTableId: String, timetableLectureIds: List): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( Update().pull( Timetable::lectures.toDotPath(), - Query.query(TimetableLecture::lectureId.inValues(lectureIds)) + Query.query(TimetableLecture::id.inValues(timetableLectureIds.map { ObjectId(it) })) ).currentDate(Timetable::updatedAt.toDotPath()), - ).findModifyAndAwait() + ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() - override suspend fun updateLecture( - timeTableId: String, - timetableLecture: TimetableLecture - ): Timetable = - reactiveMongoTemplate.update() - .matching(Timetable::id.isEqualTo(timeTableId).and("lecture_list._id").isEqualTo(timetableLecture.id)).apply( - Update().apply { set("""lecture_list.$""", timetableLecture) } - ).findModifyAndAwait() + override suspend fun updateLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = + reactiveMongoTemplate.update().matching( + Timetable::id.isEqualTo(timeTableId).and("lecture_list._id").isEqualTo(ObjectId(timetableLecture.id)) + ).apply(Update().apply { set("""lecture_list.$""", timetableLecture) }) + .withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() override suspend fun findLatestChildTimetable( userId: String, diff --git a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt index f55dcedf..badb4009 100644 --- a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt @@ -91,7 +91,10 @@ class TimetableLectureServiceImpl( val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetable.lectures.find { it.id == timetableLectureId } ?: throw LectureNotFoundException timetableLecture.apply { - courseTitle = modifyTimetableLectureRequestDto.courseTitle + courseTitle = modifyTimetableLectureRequestDto.courseTitle ?: courseTitle + academicYear = modifyTimetableLectureRequestDto.academicYear ?: academicYear + category = modifyTimetableLectureRequestDto.category ?: category + classification = modifyTimetableLectureRequestDto.classification ?: classification instructor = modifyTimetableLectureRequestDto.instructor ?: instructor credit = modifyTimetableLectureRequestDto.credit ?: credit remark = modifyTimetableLectureRequestDto.remark ?: remark @@ -125,7 +128,7 @@ class TimetableLectureServiceImpl( } overlappingLectures.isNotEmpty() && isForced -> { - timetableRepository.pullLectures(timetable.id!!, overlappingLectures.map { it.id!! }) + timetableRepository.pullLectures(timetable.id!!, overlappingLectures.map { it.id }) } } return timetableRepository.pushLecture(timetable.id!!, timetableLecture) diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index 6b0602d1..55868c55 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -11,7 +11,7 @@ spring: database: logging: level: - org.springframework.data.mongodb.core.MongoTemplate: DEBUG + org.springframework.data.mongodb.core.ReactiveMongoTemplate: DEBUG google: firebase: From 6703290c0420633e9efa5d8ba3f1fc4611a6a976 Mon Sep 17 00:00:00 2001 From: Hank-Choi Date: Sun, 7 Jan 2024 04:58:58 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=B6=94=EA=B0=80,=EB=B3=80=EA=B2=BD=20=EC=8B=9C?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EA=B2=B9=EC=B9=98=EB=8A=94=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=B2=B4=ED=81=AC=20=EB=B2=84=EA=B7=B8=20=ED=94=BD?= =?UTF-8?q?=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/main/kotlin/filter/ErrorWebFilter.kt | 3 +- .../kotlin/handler/TimetableLectureHandler.kt | 2 + .../kotlin/common/exception/Snu4tException.kt | 8 +++- ...stomTimetableLectureAddLegacyRequestDto.kt | 4 -- .../service/TimetableLectureService.kt | 37 +++++++++++++------ 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/api/src/main/kotlin/filter/ErrorWebFilter.kt b/api/src/main/kotlin/filter/ErrorWebFilter.kt index a557a984..67b5316a 100644 --- a/api/src/main/kotlin/filter/ErrorWebFilter.kt +++ b/api/src/main/kotlin/filter/ErrorWebFilter.kt @@ -58,7 +58,7 @@ class ErrorWebFilter( private fun makeErrorBody( exception: Snu4tException, ): ErrorBody { - return ErrorBody(exception.error.errorCode, exception.errorMessage, exception.displayMessage) + return ErrorBody(exception.error.errorCode, exception.errorMessage, exception.displayMessage, exception.ext) } } @@ -66,5 +66,6 @@ private data class ErrorBody( val errcode: Long, val message: String, val displayMessage: String, + // TODO: 구버전 대응용 ext 필드. 추후 삭제 val ext: Map = mapOf(), ) diff --git a/api/src/main/kotlin/handler/TimetableLectureHandler.kt b/api/src/main/kotlin/handler/TimetableLectureHandler.kt index 851297c4..0091bee5 100644 --- a/api/src/main/kotlin/handler/TimetableLectureHandler.kt +++ b/api/src/main/kotlin/handler/TimetableLectureHandler.kt @@ -51,11 +51,13 @@ class TimetableLectureHandler( val userId = req.userId val timetableId = req.pathVariable("timetableId") val timetableLectureId = req.pathVariable("timetableLectureId") + val isForced = req.awaitBodyOrNull()?.isForced ?: false timetableLectureService.resetTimetableLecture( userId = userId, timetableId = timetableId, timetableLectureId = timetableLectureId, + isForced, ).let(::TimetableLegacyDto) } diff --git a/core/src/main/kotlin/common/exception/Snu4tException.kt b/core/src/main/kotlin/common/exception/Snu4tException.kt index c2f8b9f3..8a2e5edb 100644 --- a/core/src/main/kotlin/common/exception/Snu4tException.kt +++ b/core/src/main/kotlin/common/exception/Snu4tException.kt @@ -4,6 +4,8 @@ open class Snu4tException( val error: ErrorType = ErrorType.DEFAULT_ERROR, val errorMessage: String = error.errorMessage, val displayMessage: String = error.displayMessage, + // TODO: 구버전 대응용 ext 필드. 추후 삭제 + val ext: Map = mapOf(), ) : RuntimeException(errorMessage) object InvalidTimeException : Snu4tException(ErrorType.INVALID_TIME) @@ -48,7 +50,11 @@ object InvalidRegistrationForPreviousSemesterCourseException : object DuplicateTimetableTitleException : Snu4tException(ErrorType.DUPLICATE_TIMETABLE_TITLE) object DuplicateTimetableLectureException : Snu4tException(ErrorType.DUPLICATE_LECTURE) object WrongSemesterException : Snu4tException(ErrorType.WRONG_SEMESTER) -class LectureTimeOverlapException(confirmMessage: String) : Snu4tException(ErrorType.LECTURE_TIME_OVERLAP, confirmMessage, confirmMessage) +class LectureTimeOverlapException(confirmMessage: String) : Snu4tException( + error = ErrorType.LECTURE_TIME_OVERLAP, + displayMessage = confirmMessage, + ext = mapOf("confirm_message" to confirmMessage) +) object CustomLectureResetException : Snu4tException(ErrorType.CANNOT_RESET_CUSTOM_LECTURE) object TimetableNotFoundException : Snu4tException(ErrorType.TIMETABLE_NOT_FOUND) object PrimaryTimetableNotFoundException : Snu4tException(ErrorType.TIMETABLE_NOT_FOUND) diff --git a/core/src/main/kotlin/timetables/dto/request/CustomTimetableLectureAddLegacyRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/CustomTimetableLectureAddLegacyRequestDto.kt index 392c03d5..3d29cc15 100644 --- a/core/src/main/kotlin/timetables/dto/request/CustomTimetableLectureAddLegacyRequestDto.kt +++ b/core/src/main/kotlin/timetables/dto/request/CustomTimetableLectureAddLegacyRequestDto.kt @@ -1,8 +1,6 @@ package com.wafflestudio.snu4t.timetables.dto.request import com.fasterxml.jackson.annotation.JsonProperty -import com.wafflestudio.snu4t.common.exception.InvalidTimeException -import com.wafflestudio.snu4t.lectures.utils.ClassTimeUtils import com.wafflestudio.snu4t.timetables.data.ColorSet import com.wafflestudio.snu4t.timetables.data.TimetableLecture @@ -21,8 +19,6 @@ data class CustomTimetableLectureAddLegacyRequestDto( ) { fun toTimetableLecture(): TimetableLecture { val classPlaceAndTimes = this.classPlaceAndTimes.map { it.toClassPlaceAndTime() } - val isTimesOverlapped = ClassTimeUtils.timesOverlap(classPlaceAndTimes) - if (isTimesOverlapped) throw InvalidTimeException return TimetableLecture( courseTitle = courseTitle, instructor = instructor, diff --git a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt index badb4009..629b0e43 100644 --- a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt @@ -2,6 +2,7 @@ package com.wafflestudio.snu4t.timetables.service import com.wafflestudio.snu4t.common.exception.CustomLectureResetException import com.wafflestudio.snu4t.common.exception.DuplicateTimetableLectureException +import com.wafflestudio.snu4t.common.exception.InvalidTimeException import com.wafflestudio.snu4t.common.exception.LectureNotFoundException import com.wafflestudio.snu4t.common.exception.LectureTimeOverlapException import com.wafflestudio.snu4t.common.exception.TimetableNotFoundException @@ -25,7 +26,12 @@ interface TimetableLectureService { isForced: Boolean ): Timetable - suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable + suspend fun resetTimetableLecture( + userId: String, + timetableId: String, + timetableLectureId: String, + isForced: Boolean + ): Timetable suspend fun modifyTimetableLecture( userId: String, timetableId: String, @@ -45,12 +51,12 @@ class TimetableLectureServiceImpl( override suspend fun addLecture(userId: String, timetableId: String, lectureId: String, isForced: Boolean): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val lecture = lectureRepository.findById(lectureId) ?: throw LectureNotFoundException - if (!(timetable.year == lecture.year && timetable.semester == lecture.semester)) { - throw WrongSemesterException - } + if (!(timetable.year == lecture.year && timetable.semester == lecture.semester)) throw WrongSemesterException val colorIndex = ColorUtils.getLeastUsedColorIndexByRandom(timetable.lectures.map { it.colorIndex }) if (timetable.lectures.any { it.lectureId == lectureId }) throw DuplicateTimetableLectureException - return addTimetableLecture(timetable, TimetableLecture(lecture, colorIndex), isForced) + val timetableLecture = TimetableLecture(lecture, colorIndex) + resolveTimeConflict(timetable, timetableLecture, isForced) + return timetableRepository.pushLecture(timetable.id!!, timetableLecture) } override suspend fun addCustomTimetableLecture( @@ -61,10 +67,17 @@ class TimetableLectureServiceImpl( ): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetableLectureRequest.toTimetableLecture() - return addTimetableLecture(timetable, timetableLecture, isForced) + if (ClassTimeUtils.timesOverlap(timetableLecture.classPlaceAndTimes)) throw InvalidTimeException + resolveTimeConflict(timetable, timetableLecture, isForced) + return timetableRepository.pushLecture(timetable.id!!, timetableLecture) } - override suspend fun resetTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable { + override suspend fun resetTimetableLecture( + userId: String, + timetableId: String, + timetableLectureId: String, + isForced: Boolean + ): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetable.lectures.find { it.id == timetableLectureId } ?: throw LectureNotFoundException val originalLectureId = timetableLecture.lectureId ?: throw CustomLectureResetException @@ -78,6 +91,7 @@ class TimetableLectureServiceImpl( remark = originalLecture.remark classPlaceAndTimes = originalLecture.classPlaceAndTimes } + resolveTimeConflict(timetable, timetableLecture, isForced) return timetableRepository.updateLecture(timetableId, timetableLecture) } @@ -90,6 +104,7 @@ class TimetableLectureServiceImpl( ): Timetable { val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val timetableLecture = timetable.lectures.find { it.id == timetableLectureId } ?: throw LectureNotFoundException + if (ClassTimeUtils.timesOverlap(timetableLecture.classPlaceAndTimes)) throw InvalidTimeException timetableLecture.apply { courseTitle = modifyTimetableLectureRequestDto.courseTitle ?: courseTitle academicYear = modifyTimetableLectureRequestDto.academicYear ?: academicYear @@ -102,6 +117,7 @@ class TimetableLectureServiceImpl( colorIndex = modifyTimetableLectureRequestDto.colorIndex ?: colorIndex classPlaceAndTimes = modifyTimetableLectureRequestDto.classPlaceAndTimes?.map { it.toClassPlaceAndTime() } ?: classPlaceAndTimes } + resolveTimeConflict(timetable, timetableLecture, isForced) return timetableRepository.updateLecture(timetableId, timetableLecture) } @@ -110,11 +126,11 @@ class TimetableLectureServiceImpl( return timetableRepository.pullLecture(timetableId, timetableLectureId) } - private suspend fun addTimetableLecture( + private suspend fun resolveTimeConflict( timetable: Timetable, timetableLecture: TimetableLecture, isForced: Boolean - ): Timetable { + ) { val overlappingLectures = timetable.lectures.filter { timetableLecture.id != it.id && ClassTimeUtils.timesOverlap( @@ -131,13 +147,12 @@ class TimetableLectureServiceImpl( timetableRepository.pullLectures(timetable.id!!, overlappingLectures.map { it.id }) } } - return timetableRepository.pushLecture(timetable.id!!, timetableLecture) } private fun makeOverwritingConfirmMessage(overlappingLectures: List): String { val overlappingLectureTitles = overlappingLectures.map { "'${it.courseTitle}'" }.take(2).joinToString(", ") val shortFormOfTitles = if (overlappingLectures.size < 3) "" else "외 ${overlappingLectures.size - 2}개의 " - return "$overlappingLectureTitles ${shortFormOfTitles}강의가 중복되어 있습니다. 강의를 덮어쓰시겠습니까?" + return "$overlappingLectureTitles ${shortFormOfTitles}강의와 시간이 겹칩니다. 강의를 덮어쓰시겠습니까?" } } From 062c5609373f99042c68dcf1bf82a9dce3e99d85 Mon Sep 17 00:00:00 2001 From: Davin Byeon Date: Wed, 10 Jan 2024 05:41:47 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=20(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/handler/TimetableHandler.kt | 4 +- .../kotlin/handler/TimetableThemeHandler.kt | 74 ++++++++ api/src/main/kotlin/router/MainRouter.kt | 18 ++ api/src/main/kotlin/router/docs/ThemeDocs.kt | 91 +++++++++ .../main/kotlin/common/enum/BasicThemeType.kt | 37 ++++ .../main/kotlin/common/enum/TimetableTheme.kt | 36 ---- .../main/kotlin/common/exception/ErrorType.kt | 4 + .../kotlin/common/exception/Snu4tException.kt | 4 + .../main/kotlin/timetables/data/Timetable.kt | 5 +- .../timetables/data/TimetableLecture.kt | 4 +- .../kotlin/timetables/data/TimetableTheme.kt | 25 +++ .../kotlin/timetables/dto/TimetableDto.kt | 10 +- .../timetables/dto/TimetableThemeDto.kt | 27 +++ .../request/TimetableModifyThemeRequestDto.kt | 5 +- .../request/TimetableThemeAddRequestDto.kt | 8 + .../request/TimetableThemeModifyRequestDto.kt | 8 + .../TimetableThemeCustomRepository.kt | 28 +++ .../repository/TimetableThemeRepository.kt | 14 ++ .../service/TimetableLectureService.kt | 12 +- .../timetables/service/TimetableService.kt | 40 +++- .../service/TimetableThemeService.kt | 174 ++++++++++++++++++ .../kotlin/fixture/TimetableFixture.kt | 5 +- 22 files changed, 575 insertions(+), 58 deletions(-) create mode 100644 api/src/main/kotlin/handler/TimetableThemeHandler.kt create mode 100644 api/src/main/kotlin/router/docs/ThemeDocs.kt create mode 100644 core/src/main/kotlin/common/enum/BasicThemeType.kt delete mode 100644 core/src/main/kotlin/common/enum/TimetableTheme.kt create mode 100644 core/src/main/kotlin/timetables/data/TimetableTheme.kt create mode 100644 core/src/main/kotlin/timetables/dto/TimetableThemeDto.kt create mode 100644 core/src/main/kotlin/timetables/dto/request/TimetableThemeAddRequestDto.kt create mode 100644 core/src/main/kotlin/timetables/dto/request/TimetableThemeModifyRequestDto.kt create mode 100644 core/src/main/kotlin/timetables/repository/TimetableThemeCustomRepository.kt create mode 100644 core/src/main/kotlin/timetables/repository/TimetableThemeRepository.kt create mode 100644 core/src/main/kotlin/timetables/service/TimetableThemeService.kt diff --git a/api/src/main/kotlin/handler/TimetableHandler.kt b/api/src/main/kotlin/handler/TimetableHandler.kt index 85ae21a1..915a019e 100644 --- a/api/src/main/kotlin/handler/TimetableHandler.kt +++ b/api/src/main/kotlin/handler/TimetableHandler.kt @@ -97,9 +97,9 @@ class TimetableHandler( suspend fun modifyTimetableTheme(req: ServerRequest): ServerResponse = handle(req) { val userId = req.userId val timetableId = req.pathVariable("timetableId") - val theme = req.awaitBody().theme + val body = req.awaitBody() - timetableService.modifyTimetableTheme(userId, timetableId, theme).let(::TimetableLegacyDto) + timetableService.modifyTimetableTheme(userId, timetableId, body.theme, body.themeId).let(::TimetableLegacyDto) } suspend fun setPrimary(req: ServerRequest): ServerResponse = handle(req) { diff --git a/api/src/main/kotlin/handler/TimetableThemeHandler.kt b/api/src/main/kotlin/handler/TimetableThemeHandler.kt new file mode 100644 index 00000000..e4e40cc9 --- /dev/null +++ b/api/src/main/kotlin/handler/TimetableThemeHandler.kt @@ -0,0 +1,74 @@ +package com.wafflestudio.snu4t.handler + +import com.wafflestudio.snu4t.common.enum.BasicThemeType +import com.wafflestudio.snu4t.common.exception.InvalidPathParameterException +import com.wafflestudio.snu4t.middleware.SnuttRestApiDefaultMiddleware +import com.wafflestudio.snu4t.timetables.dto.TimetableThemeDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableThemeAddRequestDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableThemeModifyRequestDto +import com.wafflestudio.snu4t.timetables.service.TimetableThemeService +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.server.ServerRequest +import org.springframework.web.reactive.function.server.awaitBody + +@Component +class TimetableThemeHandler( + private val timetableThemeService: TimetableThemeService, + snuttRestApiDefaultMiddleware: SnuttRestApiDefaultMiddleware, +) : ServiceHandler(snuttRestApiDefaultMiddleware) { + suspend fun getThemes(req: ServerRequest) = handle(req) { + val userId = req.userId + + timetableThemeService.getThemes(userId).map(::TimetableThemeDto) + } + + suspend fun addTheme(req: ServerRequest) = handle(req) { + val userId = req.userId + val body = req.awaitBody() + + timetableThemeService.addTheme(userId, body.name, body.colors).let(::TimetableThemeDto) + } + + suspend fun modifyTheme(req: ServerRequest) = handle(req) { + val userId = req.userId + val themeId = req.pathVariable("themeId") + val body = req.awaitBody() + + timetableThemeService.modifyTheme(userId, themeId, body.name, body.colors).let(::TimetableThemeDto) + } + + suspend fun deleteTheme(req: ServerRequest) = handle(req) { + val userId = req.userId + val themeId = req.pathVariable("themeId") + + timetableThemeService.deleteTheme(userId, themeId) + } + + suspend fun copyTheme(req: ServerRequest) = handle(req) { + val userId = req.userId + val themeId = req.pathVariable("themeId") + + timetableThemeService.copyTheme(userId, themeId).let(::TimetableThemeDto) + } + + suspend fun setDefault(req: ServerRequest) = handle(req) { + val userId = req.userId + val themeId = req.pathVariable("themeId") + + timetableThemeService.setDefault(userId, themeId).let(::TimetableThemeDto) + } + + suspend fun setBasicThemeTypeDefault(req: ServerRequest) = handle(req) { + val userId = req.userId + val basicThemeType = req.pathVariable("basicThemeTypeValue").toIntOrNull()?.let { BasicThemeType.from(it) } ?: throw InvalidPathParameterException("basicThemeTypeValue") + + timetableThemeService.setDefault(userId, basicThemeType = basicThemeType).let(::TimetableThemeDto) + } + + suspend fun unsetDefault(req: ServerRequest) = handle(req) { + val userId = req.userId + val themeId = req.pathVariable("themeId") + + timetableThemeService.unsetDefault(userId, themeId).let(::TimetableThemeDto) + } +} diff --git a/api/src/main/kotlin/router/MainRouter.kt b/api/src/main/kotlin/router/MainRouter.kt index 8b478576..eff7d2ef 100644 --- a/api/src/main/kotlin/router/MainRouter.kt +++ b/api/src/main/kotlin/router/MainRouter.kt @@ -11,6 +11,7 @@ import com.wafflestudio.snu4t.handler.LectureSearchHandler import com.wafflestudio.snu4t.handler.NotificationHandler import com.wafflestudio.snu4t.handler.TimetableHandler import com.wafflestudio.snu4t.handler.TimetableLectureHandler +import com.wafflestudio.snu4t.handler.TimetableThemeHandler import com.wafflestudio.snu4t.handler.UserHandler import com.wafflestudio.snu4t.handler.VacancyNotifcationHandler import com.wafflestudio.snu4t.router.docs.AdminDocs @@ -20,6 +21,7 @@ import com.wafflestudio.snu4t.router.docs.ConfigDocs import com.wafflestudio.snu4t.router.docs.FriendDocs import com.wafflestudio.snu4t.router.docs.LectureSearchDocs import com.wafflestudio.snu4t.router.docs.NotificationDocs +import com.wafflestudio.snu4t.router.docs.ThemeDocs import com.wafflestudio.snu4t.router.docs.TimetableDocs import com.wafflestudio.snu4t.router.docs.UserDocs import com.wafflestudio.snu4t.router.docs.VacancyNotificationDocs @@ -39,6 +41,7 @@ class MainRouter( private val vacancyNotificationHandler: VacancyNotifcationHandler, private val timeTableHandler: TimetableHandler, private val timeTableLectureHandler: TimetableLectureHandler, + private val timetableThemeHandler: TimetableThemeHandler, private val bookmarkHandler: BookmarkHandler, private val lectureSearchHandler: LectureSearchHandler, private val friendHandler: FriendHandler, @@ -174,6 +177,21 @@ class MainRouter( } } + @Bean + @ThemeDocs + fun timetableThemeRoute() = v1CoRouter { + "/themes".nest { + GET("", timetableThemeHandler::getThemes) + POST("", timetableThemeHandler::addTheme) + PATCH("{themeId}", timetableThemeHandler::modifyTheme) + DELETE("{themeId}", timetableThemeHandler::deleteTheme) + POST("{themeId}/copy", timetableThemeHandler::copyTheme) + POST("{themeId}/default", timetableThemeHandler::setDefault) + POST("basic/{basicThemeTypeValue}/default", timetableThemeHandler::setBasicThemeTypeDefault) + DELETE("{themeId}/default", timetableThemeHandler::unsetDefault) + } + } + private fun v1CoRouter(r: CoRouterFunctionDsl.() -> Unit) = coRouter { path("/v1").or("").nest(r) } diff --git a/api/src/main/kotlin/router/docs/ThemeDocs.kt b/api/src/main/kotlin/router/docs/ThemeDocs.kt new file mode 100644 index 00000000..cf93006b --- /dev/null +++ b/api/src/main/kotlin/router/docs/ThemeDocs.kt @@ -0,0 +1,91 @@ +package com.wafflestudio.snu4t.router.docs + +import com.wafflestudio.snu4t.timetables.dto.TimetableThemeDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableThemeAddRequestDto +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.ParameterIn +import io.swagger.v3.oas.annotations.media.ArraySchema +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.springdoc.core.annotations.RouterOperation +import org.springdoc.core.annotations.RouterOperations +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.RequestMethod + +@RouterOperations( + RouterOperation( + path = "/v1/themes", method = [RequestMethod.GET], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "getThemes", + parameters = [Parameter(`in` = ParameterIn.QUERY, name = "state", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableThemeDto::class)))])], + ), + ), + RouterOperation( + path = "/v1/themes", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "addTheme", + requestBody = RequestBody( + content = [Content(schema = Schema(implementation = TimetableThemeAddRequestDto::class))], + required = true, + ), + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), + RouterOperation( + path = "/v1/themes/{themeId}", method = [RequestMethod.PATCH], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "modifyTheme", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], + requestBody = RequestBody( + content = [Content(schema = Schema(implementation = TimetableThemeAddRequestDto::class))], + required = true, + ), + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), + RouterOperation( + path = "/v1/themes/{themeId}", method = [RequestMethod.DELETE], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "deleteTheme", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema())])], + ), + ), + RouterOperation( + path = "/v1/themes/{themeId}/copy", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "copyTheme", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), + RouterOperation( + path = "/v1/themes/{themeId}/default", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "setDefault", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), + RouterOperation( + path = "/v1/themes/basic/{basicThemeTypeValue}/default", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "setBasicThemeTypeDefault", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "basicThemeTypeValue", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), + RouterOperation( + path = "/v1/themes/{themeId}/copy", method = [RequestMethod.DELETE], produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "unsetDefault", + parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], + responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])], + ), + ), +) +annotation class ThemeDocs diff --git a/core/src/main/kotlin/common/enum/BasicThemeType.kt b/core/src/main/kotlin/common/enum/BasicThemeType.kt new file mode 100644 index 00000000..96f34247 --- /dev/null +++ b/core/src/main/kotlin/common/enum/BasicThemeType.kt @@ -0,0 +1,37 @@ +package com.wafflestudio.snu4t.common.enum + +import com.fasterxml.jackson.annotation.JsonValue +import org.springframework.core.convert.converter.Converter +import org.springframework.data.convert.ReadingConverter +import org.springframework.data.convert.WritingConverter +import org.springframework.stereotype.Component + +enum class BasicThemeType(@get:JsonValue val value: Int, val displayName: String) { + SNUTT(0, "SNUTT"), // 구 버전 호환을 위해 커스텀 테마의 경우 사용됨 + FALL(1, "가을"), + MODERN(2, "모던"), + CHERRY_BLOSSOM(3, "벚꽃"), + ICE(4, "얼음"), + LAWN(5, "잔디"), + ; + + companion object { + const val COLOR_COUNT = 9 + fun from(value: Int) = values().find { it.value == value } + fun from(displayName: String) = values().find { it.displayName == displayName } + } +} + +@ReadingConverter +@Component +class BasicThemeTypeReadConverter : Converter { + override fun convert(source: Int): BasicThemeType { + return requireNotNull(BasicThemeType.from(source)) + } +} + +@Component +@WritingConverter +class BasicThemeTypeWriteConverter : Converter { + override fun convert(source: BasicThemeType): Int = source.value +} diff --git a/core/src/main/kotlin/common/enum/TimetableTheme.kt b/core/src/main/kotlin/common/enum/TimetableTheme.kt deleted file mode 100644 index 90c04ea3..00000000 --- a/core/src/main/kotlin/common/enum/TimetableTheme.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.wafflestudio.snu4t.common.enum - -import com.fasterxml.jackson.annotation.JsonValue -import org.springframework.core.convert.converter.Converter -import org.springframework.data.convert.ReadingConverter -import org.springframework.data.convert.WritingConverter -import org.springframework.stereotype.Component - -enum class TimetableTheme(@get:JsonValue val value: Int) { - SNUTT(0), - FALL(1), - MODERN(2), - CHERRY_BLOSSOM(3), - ICE(4), - LAWN(5), - ; - - companion object { - private val valueMap = TimetableTheme.values().associateBy { e -> e.value } - fun from(value: Int) = valueMap[value] - } -} - -@ReadingConverter -@Component -class TimetableThemeReadConverter : Converter { - override fun convert(source: Int): TimetableTheme { - return requireNotNull(TimetableTheme.from(source)) - } -} - -@Component -@WritingConverter -class TimetableThemeWriteConverter : Converter { - override fun convert(source: TimetableTheme): Int = source.value -} diff --git a/core/src/main/kotlin/common/exception/ErrorType.kt b/core/src/main/kotlin/common/exception/ErrorType.kt index b4fd3de0..a9f53de5 100644 --- a/core/src/main/kotlin/common/exception/ErrorType.kt +++ b/core/src/main/kotlin/common/exception/ErrorType.kt @@ -43,6 +43,7 @@ enum class ErrorType( INVALID_DISPLAY_NAME(HttpStatus.BAD_REQUEST, 40009, "displayName이 유효하지 않습니다.", "조건에 맞지 않는 이름입니다."), TABLE_DELETE_ERROR(HttpStatus.BAD_REQUEST, 40010, "하나 남은 시간표는 삭제할 수 없습니다."), TIMETABLE_NOT_PRIMARY(HttpStatus.BAD_REQUEST, 40011, "대표 시간표가 아닙니다."), + INVALID_THEME_COLOR_COUNT(HttpStatus.BAD_REQUEST, 40012, "테마의 색상 개수가 적절하지 않습니다.", "테마의 색상 개수가 적절하지 않습니다."), TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40400, "timetable_id가 유효하지 않습니다", "존재하지 않는 시간표입니다."), PRIMARY_TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40401, "timetable_id가 유효하지 않습니다", "대표 시간표가 존재하지 않습니다."), @@ -50,11 +51,14 @@ enum class ErrorType( CONFIG_NOT_FOUND(HttpStatus.NOT_FOUND, 40403, "config가 존재하지 않습니다."), FRIEND_NOT_FOUND(HttpStatus.NOT_FOUND, 40404, "친구 관계가 존재하지 않습니다.", "친구 관계가 존재하지 않습니다."), USER_NOT_FOUND_BY_NICKNAME(HttpStatus.NOT_FOUND, 40405, "해당 닉네임의 유저를 찾을 수 없습니다.", "해당 닉네임의 유저를 찾을 수 없습니다."), + THEME_NOT_FOUND(HttpStatus.NOT_FOUND, 40406, "테마를 찾을 수 없습니다.", "테마를 찾을 수 없습니다."), 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, "중복된 테마 이름입니다.", "중복된 테마 이름입니다."), + INVALID_THEME_TYPE(HttpStatus.CONFLICT, 40905, "적절하지 않은 유형의 테마입니다.", "적절하지 않은 유형의 테마입니다."), DYNAMIC_LINK_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 50001, "링크 생성 실패", "링크 생성에 실패했습니다. 잠시 후 다시 시도해주세요."), } diff --git a/core/src/main/kotlin/common/exception/Snu4tException.kt b/core/src/main/kotlin/common/exception/Snu4tException.kt index 8a2e5edb..ae00a2fb 100644 --- a/core/src/main/kotlin/common/exception/Snu4tException.kt +++ b/core/src/main/kotlin/common/exception/Snu4tException.kt @@ -42,6 +42,7 @@ object InvalidAppTypeException : Snu4tException(ErrorType.INVALID_APP_TYPE) object InvalidNicknameException : Snu4tException(ErrorType.INVALID_NICKNAME) object InvalidDisplayNameException : Snu4tException(ErrorType.INVALID_DISPLAY_NAME) object TableDeleteErrorException : Snu4tException(ErrorType.TABLE_DELETE_ERROR) +object InvalidThemeColorCountException : Snu4tException(ErrorType.INVALID_THEME_COLOR_COUNT) object NoUserFcmKeyException : Snu4tException(ErrorType.NO_USER_FCM_KEY) object InvalidRegistrationForPreviousSemesterCourseException : @@ -62,10 +63,13 @@ object TimetableNotPrimaryException : Snu4tException(ErrorType.DEFAULT_ERROR) object ConfigNotFoundException : Snu4tException(ErrorType.CONFIG_NOT_FOUND) object FriendNotFoundException : Snu4tException(ErrorType.FRIEND_NOT_FOUND) object UserNotFoundByNicknameException : Snu4tException(ErrorType.USER_NOT_FOUND_BY_NICKNAME) +object ThemeNotFoundException : Snu4tException(ErrorType.THEME_NOT_FOUND) object DuplicateVacancyNotificationException : Snu4tException(ErrorType.DUPLICATE_VACANCY_NOTIFICATION) object DuplicateEmailException : Snu4tException(ErrorType.DUPLICATE_EMAIL) object DuplicateFriendException : Snu4tException(ErrorType.DUPLICATE_FRIEND) object InvalidFriendException : Snu4tException(ErrorType.INVALID_FRIEND) +object DuplicateThemeNameException : Snu4tException(ErrorType.DUPLICATE_THEME_NAME) +object InvalidThemeTypeException : Snu4tException(ErrorType.INVALID_THEME_TYPE) object DynamicLinkGenerationFailedException : Snu4tException(ErrorType.DYNAMIC_LINK_GENERATION_FAILED) diff --git a/core/src/main/kotlin/timetables/data/Timetable.kt b/core/src/main/kotlin/timetables/data/Timetable.kt index 1a8ad878..7a1473bd 100644 --- a/core/src/main/kotlin/timetables/data/Timetable.kt +++ b/core/src/main/kotlin/timetables/data/Timetable.kt @@ -1,8 +1,8 @@ package com.wafflestudio.snu4t.timetables.data import com.fasterxml.jackson.annotation.JsonProperty +import com.wafflestudio.snu4t.common.enum.BasicThemeType import com.wafflestudio.snu4t.common.enum.Semester -import com.wafflestudio.snu4t.common.enum.TimetableTheme import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.index.Indexed import org.springframework.data.mongodb.core.mapping.Document @@ -24,7 +24,8 @@ data class Timetable( @JsonProperty("lecture_list") var lectures: List = emptyList(), var title: String, - var theme: TimetableTheme, + var theme: BasicThemeType, + var themeId: String?, @Field("is_primary") var isPrimary: Boolean? = null, @Field("updated_at") diff --git a/core/src/main/kotlin/timetables/data/TimetableLecture.kt b/core/src/main/kotlin/timetables/data/TimetableLecture.kt index 5c59d6f6..e86d22de 100644 --- a/core/src/main/kotlin/timetables/data/TimetableLecture.kt +++ b/core/src/main/kotlin/timetables/data/TimetableLecture.kt @@ -45,7 +45,7 @@ data class TimetableLecture( var lectureId: String? = null, ) -fun TimetableLecture(lecture: Lecture, colorIndex: Int) = TimetableLecture( +fun TimetableLecture(lecture: Lecture, colorIndex: Int, color: ColorSet?) = TimetableLecture( lectureId = lecture.id, academicYear = lecture.academicYear, category = lecture.category, @@ -61,5 +61,5 @@ fun TimetableLecture(lecture: Lecture, colorIndex: Int) = TimetableLecture( courseNumber = lecture.courseNumber, courseTitle = lecture.courseTitle, colorIndex = colorIndex, - color = null, + color = color, ) diff --git a/core/src/main/kotlin/timetables/data/TimetableTheme.kt b/core/src/main/kotlin/timetables/data/TimetableTheme.kt new file mode 100644 index 00000000..3d5aa3e5 --- /dev/null +++ b/core/src/main/kotlin/timetables/data/TimetableTheme.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.snu4t.timetables.data + +import org.springframework.data.annotation.Id +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, + @Indexed + @Field(targetType = FieldType.OBJECT_ID) + val userId: String, + var name: String, + var colors: List?, // basic 테마는 null (클라이언트 처리) + val isCustom: Boolean, // basic 테마는 false + var isDefault: Boolean, + val createdAt: LocalDateTime = LocalDateTime.now(), + var updatedAt: LocalDateTime = LocalDateTime.now(), +) diff --git a/core/src/main/kotlin/timetables/dto/TimetableDto.kt b/core/src/main/kotlin/timetables/dto/TimetableDto.kt index 801f45af..41813f87 100644 --- a/core/src/main/kotlin/timetables/dto/TimetableDto.kt +++ b/core/src/main/kotlin/timetables/dto/TimetableDto.kt @@ -1,8 +1,8 @@ package com.wafflestudio.snu4t.timetables.dto import com.fasterxml.jackson.annotation.JsonProperty +import com.wafflestudio.snu4t.common.enum.BasicThemeType import com.wafflestudio.snu4t.common.enum.Semester -import com.wafflestudio.snu4t.common.enum.TimetableTheme import com.wafflestudio.snu4t.timetables.data.Timetable import java.time.Instant @@ -13,7 +13,8 @@ data class TimetableDto( var semester: Semester, var lectures: List = emptyList(), var title: String, - val theme: TimetableTheme, + val theme: BasicThemeType, + val themeId: String?, val isPrimary: Boolean, var updatedAt: Instant = Instant.now(), ) @@ -26,6 +27,7 @@ fun TimetableDto(timetable: Timetable) = TimetableDto( lectures = timetable.lectures.map { TimetableLectureDto(it) }, title = timetable.title, theme = timetable.theme, + themeId = timetable.themeId, isPrimary = timetable.isPrimary ?: false, updatedAt = timetable.updatedAt, ) @@ -40,7 +42,8 @@ data class TimetableLegacyDto( @JsonProperty("lecture_list") var lectures: List = emptyList(), var title: String, - val theme: TimetableTheme, + val theme: BasicThemeType, + val themeId: String?, val isPrimary: Boolean, @JsonProperty("updated_at") var updatedAt: Instant = Instant.now(), @@ -54,6 +57,7 @@ fun TimetableLegacyDto(timetable: Timetable) = TimetableLegacyDto( lectures = timetable.lectures.map { TimetableLectureLegacyDto(it) }, title = timetable.title, theme = timetable.theme, + themeId = timetable.themeId, isPrimary = timetable.isPrimary ?: false, updatedAt = timetable.updatedAt, ) diff --git a/core/src/main/kotlin/timetables/dto/TimetableThemeDto.kt b/core/src/main/kotlin/timetables/dto/TimetableThemeDto.kt new file mode 100644 index 00000000..f03a2043 --- /dev/null +++ b/core/src/main/kotlin/timetables/dto/TimetableThemeDto.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.snu4t.timetables.dto + +import com.wafflestudio.snu4t.common.enum.BasicThemeType +import com.wafflestudio.snu4t.timetables.data.ColorSet +import com.wafflestudio.snu4t.timetables.data.TimetableTheme +import com.wafflestudio.snu4t.timetables.service.toBasicThemeType + +data class TimetableThemeDto( + val id: String?, + val theme: BasicThemeType, + val name: String, + val colors: List?, + val isDefault: Boolean, + val isCustom: Boolean, +) + +fun TimetableThemeDto(timetableTheme: TimetableTheme) = + with(timetableTheme) { + TimetableThemeDto( + id = id, + theme = toBasicThemeType(), + name = name, + colors = colors, + isDefault = isDefault, + isCustom = isCustom, + ) + } diff --git a/core/src/main/kotlin/timetables/dto/request/TimetableModifyThemeRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/TimetableModifyThemeRequestDto.kt index 3bf48e3c..ed6860f7 100644 --- a/core/src/main/kotlin/timetables/dto/request/TimetableModifyThemeRequestDto.kt +++ b/core/src/main/kotlin/timetables/dto/request/TimetableModifyThemeRequestDto.kt @@ -1,7 +1,8 @@ package com.wafflestudio.snu4t.timetables.dto.request -import com.wafflestudio.snu4t.common.enum.TimetableTheme +import com.wafflestudio.snu4t.common.enum.BasicThemeType data class TimetableModifyThemeRequestDto( - val theme: TimetableTheme, + val theme: BasicThemeType?, + val themeId: String?, ) diff --git a/core/src/main/kotlin/timetables/dto/request/TimetableThemeAddRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/TimetableThemeAddRequestDto.kt new file mode 100644 index 00000000..d67bcb78 --- /dev/null +++ b/core/src/main/kotlin/timetables/dto/request/TimetableThemeAddRequestDto.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.snu4t.timetables.dto.request + +import com.wafflestudio.snu4t.timetables.data.ColorSet + +data class TimetableThemeAddRequestDto( + val name: String, + val colors: List, +) diff --git a/core/src/main/kotlin/timetables/dto/request/TimetableThemeModifyRequestDto.kt b/core/src/main/kotlin/timetables/dto/request/TimetableThemeModifyRequestDto.kt new file mode 100644 index 00000000..35d6299b --- /dev/null +++ b/core/src/main/kotlin/timetables/dto/request/TimetableThemeModifyRequestDto.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.snu4t.timetables.dto.request + +import com.wafflestudio.snu4t.timetables.data.ColorSet + +data class TimetableThemeModifyRequestDto( + val name: String?, + val colors: List?, +) diff --git a/core/src/main/kotlin/timetables/repository/TimetableThemeCustomRepository.kt b/core/src/main/kotlin/timetables/repository/TimetableThemeCustomRepository.kt new file mode 100644 index 00000000..6fecd2bb --- /dev/null +++ b/core/src/main/kotlin/timetables/repository/TimetableThemeCustomRepository.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.snu4t.timetables.repository + +import com.wafflestudio.snu4t.common.extension.desc +import com.wafflestudio.snu4t.common.extension.isEqualTo +import com.wafflestudio.snu4t.common.extension.regex +import com.wafflestudio.snu4t.timetables.data.TimetableTheme +import kotlinx.coroutines.reactor.awaitSingleOrNull +import org.springframework.data.mongodb.core.ReactiveMongoTemplate +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.and + +interface TimetableThemeCustomRepository { + suspend fun findLastChild(userId: String, name: String): TimetableTheme? +} + +class TimetableThemeCustomRepositoryImpl( + private val reactiveMongoTemplate: ReactiveMongoTemplate, +) : TimetableThemeCustomRepository { + override suspend fun findLastChild(userId: String, name: String): TimetableTheme? { + return reactiveMongoTemplate.findOne( + Query.query( + TimetableTheme::userId isEqualTo userId and + TimetableTheme::name regex """${Regex.escape(name)}(\s+\(\d+\))?""" + ).with(TimetableTheme::name.desc()), + TimetableTheme::class.java + ).awaitSingleOrNull() + } +} diff --git a/core/src/main/kotlin/timetables/repository/TimetableThemeRepository.kt b/core/src/main/kotlin/timetables/repository/TimetableThemeRepository.kt new file mode 100644 index 00000000..645749b7 --- /dev/null +++ b/core/src/main/kotlin/timetables/repository/TimetableThemeRepository.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.snu4t.timetables.repository + +import com.wafflestudio.snu4t.timetables.data.TimetableTheme +import org.springframework.data.repository.kotlin.CoroutineCrudRepository + +interface TimetableThemeRepository : CoroutineCrudRepository, TimetableThemeCustomRepository { + suspend fun findByIdAndUserId(id: String, userId: String): TimetableTheme? + + suspend fun findByUserIdAndIsDefaultTrue(userId: String): TimetableTheme? + + suspend fun findByUserIdAndIsCustomTrueOrderByCreatedAtDesc(userId: String): List + + suspend fun existsByUserIdAndName(userId: String, name: String): Boolean +} diff --git a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt index 629b0e43..c1b04342 100644 --- a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt @@ -45,6 +45,7 @@ interface TimetableLectureService { @Service class TimetableLectureServiceImpl( + private val timetableThemeService: TimetableThemeService, private val timetableRepository: TimetableRepository, private val lectureRepository: LectureRepository, ) : TimetableLectureService { @@ -52,9 +53,16 @@ class TimetableLectureServiceImpl( val timetable = timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException val lecture = lectureRepository.findById(lectureId) ?: throw LectureNotFoundException if (!(timetable.year == lecture.year && timetable.semester == lecture.semester)) throw WrongSemesterException - val colorIndex = ColorUtils.getLeastUsedColorIndexByRandom(timetable.lectures.map { it.colorIndex }) + + val (colorIndex, color) = if (timetable.themeId == null) { + ColorUtils.getLeastUsedColorIndexByRandom(timetable.lectures.map { it.colorIndex }) to null + } else { + val theme = timetableThemeService.getTheme(userId, timetable.themeId) + 0 to requireNotNull(theme.colors).random() + } + if (timetable.lectures.any { it.lectureId == lectureId }) throw DuplicateTimetableLectureException - val timetableLecture = TimetableLecture(lecture, colorIndex) + val timetableLecture = TimetableLecture(lecture, colorIndex, color) resolveTimeConflict(timetable, timetableLecture, isForced) return timetableRepository.pushLecture(timetable.id!!, timetableLecture) } diff --git a/core/src/main/kotlin/timetables/service/TimetableService.kt b/core/src/main/kotlin/timetables/service/TimetableService.kt index cbea4621..eb5e51a2 100644 --- a/core/src/main/kotlin/timetables/service/TimetableService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableService.kt @@ -2,8 +2,8 @@ package com.wafflestudio.snu4t.timetables.service import com.wafflestudio.snu4t.common.dynamiclink.client.DynamicLinkClient import com.wafflestudio.snu4t.common.dynamiclink.dto.DynamicLinkResponse +import com.wafflestudio.snu4t.common.enum.BasicThemeType import com.wafflestudio.snu4t.common.enum.Semester -import com.wafflestudio.snu4t.common.enum.TimetableTheme import com.wafflestudio.snu4t.common.exception.DuplicateTimetableTitleException import com.wafflestudio.snu4t.common.exception.InvalidTimetableTitleException import com.wafflestudio.snu4t.common.exception.PrimaryTimetableNotFoundException @@ -33,7 +33,7 @@ interface TimetableService { suspend fun getTimetable(userId: String, timetableId: String): Timetable suspend fun modifyTimetableTitle(userId: String, timetableId: String, title: String): Timetable suspend fun deleteTimetable(userId: String, timetableId: String) - suspend fun modifyTimetableTheme(userId: String, timetableId: String, theme: TimetableTheme): Timetable + suspend fun modifyTimetableTheme(userId: String, timetableId: String, basicThemeType: BasicThemeType?, themeId: String?): Timetable suspend fun copyTimetable(userId: String, timetableId: String, title: String? = null): Timetable suspend fun getUserPrimaryTable(userId: String, year: Int, semester: Semester): Timetable suspend fun getCoursebooksWithPrimaryTable(userId: String): List @@ -44,9 +44,10 @@ interface TimetableService { @Service class TimetableServiceImpl( + private val coursebookService: CoursebookService, + private val timetableThemeService: TimetableThemeService, private val timetableRepository: TimetableRepository, private val dynamicLinkClient: DynamicLinkClient, - private val coursebookService: CoursebookService, @Value("\${google.firebase.dynamic-link.link-prefix}") val linkPrefix: String, ) : TimetableService { override suspend fun getTimetables(userId: String): List = @@ -60,12 +61,15 @@ class TimetableServiceImpl( override suspend fun addTimetable(userId: String, timetableRequest: TimetableAddRequestDto): Timetable { validateTimetableTitle(userId, timetableRequest.year, timetableRequest.semester, timetableRequest.title) + + val defaultTheme = timetableThemeService.getDefaultTheme(userId) return Timetable( userId = userId, year = timetableRequest.year, semester = timetableRequest.semester, title = timetableRequest.title, - theme = TimetableTheme.SNUTT, + theme = defaultTheme.toBasicThemeType(), + themeId = defaultTheme?.id, isPrimary = timetableRepository .findAllByUserIdAndYearAndSemester(userId, timetableRequest.year, timetableRequest.semester) .toList() @@ -111,8 +115,27 @@ class TimetableServiceImpl( ).let { timetableRepository.save(it) } } - override suspend fun modifyTimetableTheme(userId: String, timetableId: String, theme: TimetableTheme): Timetable = - getTimetable(userId, timetableId).apply { this.theme = theme }.let { timetableRepository.save(it) } + override suspend fun modifyTimetableTheme(userId: String, timetableId: String, basicThemeType: BasicThemeType?, themeId: String?): Timetable { + require((themeId == null) xor (basicThemeType == null)) + + val timetable = getTimetable(userId, timetableId) + val theme = timetableThemeService.getTheme(userId, themeId, basicThemeType) + + timetable.theme = theme.toBasicThemeType() + timetable.themeId = theme.id + + val colorCount = if (theme.isCustom) requireNotNull(theme.colors).size else BasicThemeType.COLOR_COUNT + timetable.lectures.forEachIndexed { index, lecture -> + if (theme.isCustom) { + lecture.color = theme.colors!![index % colorCount] + lecture.colorIndex = 0 + } else { + lecture.color = null + lecture.colorIndex = (index % colorCount) + 1 + } + } + return timetableRepository.save(timetable) + } override suspend fun getUserPrimaryTable(userId: String, year: Int, semester: Semester): Timetable { return timetableRepository.findByUserIdAndYearAndSemester(userId, year, semester) @@ -130,12 +153,15 @@ class TimetableServiceImpl( override suspend fun createDefaultTable(userId: String) { val coursebook = coursebookService.getLatestCoursebook() + val defaultTheme = timetableThemeService.getDefaultTheme(userId) + val timetable = Timetable( userId = userId, year = coursebook.year, semester = coursebook.semester, title = "나의 시간표", - theme = TimetableTheme.SNUTT, + theme = defaultTheme.toBasicThemeType(), + themeId = defaultTheme?.id, ) timetableRepository.save(timetable) } diff --git a/core/src/main/kotlin/timetables/service/TimetableThemeService.kt b/core/src/main/kotlin/timetables/service/TimetableThemeService.kt new file mode 100644 index 00000000..968a5122 --- /dev/null +++ b/core/src/main/kotlin/timetables/service/TimetableThemeService.kt @@ -0,0 +1,174 @@ +package com.wafflestudio.snu4t.timetables.service + +import com.wafflestudio.snu4t.common.enum.BasicThemeType +import com.wafflestudio.snu4t.common.exception.DuplicateThemeNameException +import com.wafflestudio.snu4t.common.exception.InvalidThemeColorCountException +import com.wafflestudio.snu4t.common.exception.InvalidThemeTypeException +import com.wafflestudio.snu4t.common.exception.ThemeNotFoundException +import com.wafflestudio.snu4t.timetables.data.ColorSet +import com.wafflestudio.snu4t.timetables.data.TimetableTheme +import com.wafflestudio.snu4t.timetables.repository.TimetableThemeRepository +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +interface TimetableThemeService { + suspend fun getThemes(userId: String): List + + suspend fun addTheme(userId: String, name: String, colors: List): TimetableTheme + + suspend fun modifyTheme(userId: String, themeId: String, name: String?, colors: List?): TimetableTheme + + suspend fun deleteTheme(userId: String, themeId: String) + + suspend fun copyTheme(userId: String, themeId: String): TimetableTheme + + suspend fun setDefault(userId: String, themeId: String? = null, basicThemeType: BasicThemeType? = null): TimetableTheme + + suspend fun unsetDefault(userId: String, themeId: String): TimetableTheme + + suspend fun getDefaultTheme(userId: String): TimetableTheme? + + suspend fun getTheme(userId: String, themeId: String? = null, basicThemeType: BasicThemeType? = null): TimetableTheme +} + +@Service +class TimetableThemeServiceImpl( + private val timetableThemeRepository: TimetableThemeRepository, +) : TimetableThemeService { + companion object { + private const val MAX_COLOR_COUNT = 9 + private val copyNumberRegex = """\s\(\d+\)$""".toRegex() + } + + override suspend fun getThemes(userId: String): List { + val customThemes = timetableThemeRepository.findByUserIdAndIsCustomTrueOrderByCreatedAtDesc(userId) + val defaultTheme = getDefaultTheme(userId) + + return ( + BasicThemeType.values().map { buildTimetableTheme(userId, it, isDefault = it.displayName == defaultTheme?.name) } + + customThemes + ) + } + + override suspend fun addTheme(userId: String, name: String, colors: List): TimetableTheme { + if (colors.size !in 1..MAX_COLOR_COUNT) throw InvalidThemeColorCountException + if (timetableThemeRepository.existsByUserIdAndName(userId, name)) throw DuplicateThemeNameException + + val theme = TimetableTheme( + userId = userId, + name = name, + colors = colors, + isCustom = true, + isDefault = false, + ) + return timetableThemeRepository.save(theme) + } + + override suspend fun modifyTheme(userId: String, themeId: String, name: String?, colors: List?): TimetableTheme { + val theme = getCustomTheme(userId, themeId) + + name?.let { + if (theme.name != it && timetableThemeRepository.existsByUserIdAndName(userId, it)) throw DuplicateThemeNameException + theme.name = it + } + colors?.let { theme.colors = it } + theme.updatedAt = LocalDateTime.now() + return timetableThemeRepository.save(theme) + } + + override suspend fun deleteTheme(userId: String, themeId: String) { + val theme = getCustomTheme(userId, themeId) + timetableThemeRepository.delete(theme) + } + + override suspend fun copyTheme(userId: String, themeId: String): TimetableTheme { + val theme = getCustomTheme(userId, themeId) + + val baseName = theme.name.replace(copyNumberRegex, "") + val lastCopiedThemeNumber = getLastCopiedThemeNumber(userId, theme.name) + + val newTheme = TimetableTheme( + userId = userId, + name = "$baseName (${lastCopiedThemeNumber + 1})", + colors = theme.colors, + isCustom = true, + isDefault = false, + ) + return timetableThemeRepository.save(newTheme) + } + + private suspend fun getLastCopiedThemeNumber(userId: String, name: String): Int { + val baseName = name.replace(copyNumberRegex, "") + return timetableThemeRepository.findLastChild(userId, baseName)?.name + ?.replace(baseName, "")?.filter { it.isDigit() }?.toIntOrNull() ?: 0 + } + + override suspend fun setDefault(userId: String, themeId: String?, basicThemeType: BasicThemeType?): TimetableTheme { + require((themeId == null) xor (basicThemeType == null)) + + val theme = themeId?.let { + timetableThemeRepository.findByIdAndUserId(it, userId) ?: throw ThemeNotFoundException + } ?: buildTimetableTheme(userId, basicThemeType!!, isDefault = true) + + val defaultThemeBefore = timetableThemeRepository.findByUserIdAndIsDefaultTrue(userId) + defaultThemeBefore?.let { + if (it.isCustom) { + it.isDefault = false + it.updatedAt = LocalDateTime.now() + timetableThemeRepository.save(it) + } else { + timetableThemeRepository.delete(it) + } + } + + theme.isDefault = true + theme.updatedAt = LocalDateTime.now() + return timetableThemeRepository.save(theme) + } + + override suspend fun unsetDefault(userId: String, themeId: String): TimetableTheme { + val theme = timetableThemeRepository.findByIdAndUserId(themeId, userId) ?: throw ThemeNotFoundException + if (!theme.isDefault) return theme + + if (theme.isCustom) { + theme.isDefault = false + theme.updatedAt = LocalDateTime.now() + timetableThemeRepository.save(theme) + } else { + timetableThemeRepository.delete(theme) + } + + return timetableThemeRepository.save(buildTimetableTheme(userId, BasicThemeType.SNUTT, isDefault = true)) + } + + override suspend fun getDefaultTheme(userId: String): TimetableTheme? { + return timetableThemeRepository.findByUserIdAndIsDefaultTrue(userId) + } + + override suspend fun getTheme(userId: String, themeId: String?, basicThemeType: BasicThemeType?): TimetableTheme { + require((themeId == null) xor (basicThemeType == null)) + + val defaultTheme = getDefaultTheme(userId) + + return themeId?.let { + timetableThemeRepository.findByIdAndUserId(it, userId) ?: throw ThemeNotFoundException + } ?: buildTimetableTheme(userId, basicThemeType!!, isDefault = basicThemeType.displayName == defaultTheme?.name) + } + + private suspend fun getCustomTheme(userId: String, themeId: String): TimetableTheme { + return timetableThemeRepository.findByIdAndUserId(themeId, userId)?.also { + if (!it.isCustom) throw InvalidThemeTypeException + } ?: throw ThemeNotFoundException + } + + private fun buildTimetableTheme(userId: String, basicThemeType: BasicThemeType, isDefault: Boolean) = + TimetableTheme( + userId = userId, + name = basicThemeType.displayName, + colors = null, + isCustom = false, + isDefault = isDefault, + ) +} + +fun TimetableTheme?.toBasicThemeType() = if (this == null || isCustom) BasicThemeType.SNUTT else requireNotNull(BasicThemeType.from(name)) diff --git a/core/src/testFixtures/kotlin/fixture/TimetableFixture.kt b/core/src/testFixtures/kotlin/fixture/TimetableFixture.kt index 95dc31be..46edd5f7 100644 --- a/core/src/testFixtures/kotlin/fixture/TimetableFixture.kt +++ b/core/src/testFixtures/kotlin/fixture/TimetableFixture.kt @@ -1,7 +1,7 @@ package com.wafflestudio.snu4t.fixture +import com.wafflestudio.snu4t.common.enum.BasicThemeType import com.wafflestudio.snu4t.common.enum.Semester -import com.wafflestudio.snu4t.common.enum.TimetableTheme import com.wafflestudio.snu4t.timetables.data.Timetable import org.springframework.stereotype.Component @@ -14,7 +14,8 @@ class TimetableFixture(val userFixture: UserFixture) { year = 2023, semester = Semester.AUTUMN, title = title, - theme = TimetableTheme.SNUTT, + theme = BasicThemeType.SNUTT, + themeId = null, ) } } From f9f1b26961ff959d540b64f6aeef3f1b8baf3784 Mon Sep 17 00:00:00 2001 From: Davin Byeon Date: Thu, 11 Jan 2024 07:59:53 +0900 Subject: [PATCH 5/7] Update ThemeDocs.kt --- api/src/main/kotlin/router/docs/ThemeDocs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/kotlin/router/docs/ThemeDocs.kt b/api/src/main/kotlin/router/docs/ThemeDocs.kt index cf93006b..c6f855a9 100644 --- a/api/src/main/kotlin/router/docs/ThemeDocs.kt +++ b/api/src/main/kotlin/router/docs/ThemeDocs.kt @@ -80,7 +80,7 @@ import org.springframework.web.bind.annotation.RequestMethod ), ), RouterOperation( - path = "/v1/themes/{themeId}/copy", method = [RequestMethod.DELETE], produces = [MediaType.APPLICATION_JSON_VALUE], + path = "/v1/themes/{themeId}/default", method = [RequestMethod.DELETE], produces = [MediaType.APPLICATION_JSON_VALUE], operation = Operation( operationId = "unsetDefault", parameters = [Parameter(`in` = ParameterIn.PATH, name = "themeId", required = true)], From 44c380a9ac3b791d863664cf9b18feb1cdeae4ba Mon Sep 17 00:00:00 2001 From: Hankyeol Choi Date: Thu, 18 Jan 2024 16:59:51 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=ED=94=BD=EC=8A=A4=20(#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../job/sync/service/SugangSnuSyncService.kt | 11 +++----- .../repository/TimetableCustomRepository.kt | 25 +++++++++++++------ .../service/TimetableLectureService.kt | 12 ++++----- 3 files changed, 26 insertions(+), 22 deletions(-) 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 500fe8c8..4166ac86 100644 --- a/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt +++ b/batch/src/main/kotlin/sugangsnu/job/sync/service/SugangSnuSyncService.kt @@ -257,13 +257,8 @@ class SugangSnuSyncServiceImpl( fun dropOverlappingLectures( timetables: Flow, updatedLecture: UpdatedLecture - ) = timetables.map { - it.apply { - lectures = lectures.filter { lecture -> lecture.lectureId != updatedLecture.oldData.id } - } - }.let { - timeTableRepository.saveAll(it) - }.map { timetable -> + ) = timetables.map { timetable -> + timeTableRepository.pullTimetableLectureByLectureId(timetable.id!!, updatedLecture.oldData.id!!) TimetableLectureDeleteByOverlapResult( year = timetable.year, semester = timetable.semester, @@ -299,7 +294,7 @@ class SugangSnuSyncServiceImpl( timeTableRepository.findAllContainsLectureId( deletedLecture.year, deletedLecture.semester, deletedLecture.id!! ).map { timetable -> - timeTableRepository.pullLecture(timetable.id!!, deletedLecture.id!!) + timeTableRepository.pullTimetableLectureByLectureId(timetable.id!!, deletedLecture.id!!) TimetableLectureDeleteResult( timetable.year, timetable.semester, diff --git a/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt b/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt index 01f7c040..5bbed599 100644 --- a/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt +++ b/core/src/main/kotlin/timetables/repository/TimetableCustomRepository.kt @@ -31,10 +31,11 @@ interface TimetableCustomRepository { lectureNumber: String ): Flow - suspend fun pushLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable - suspend fun pullLecture(timeTableId: String, timetableLectureId: String): Timetable - suspend fun pullLectures(timeTableId: String, timetableLectureIds: List): Timetable - suspend fun updateLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable + suspend fun pushTimetableLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable + suspend fun pullTimetableLecture(timeTableId: String, timetableLectureId: String): Timetable + suspend fun pullTimetableLectureByLectureId(timeTableId: String, lectureId: String): Timetable + suspend fun pullTimetableLectures(timeTableId: String, timetableLectureIds: List): Timetable + suspend fun updateTimetableLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable suspend fun findLatestChildTimetable(userId: String, year: Int, semester: Semester, title: String): Timetable? } @@ -69,12 +70,12 @@ class TimetableCustomRepositoryImpl( ).asFlow() } - override suspend fun pushLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = + override suspend fun pushTimetableLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( Update().push(Timetable::lectures.toDotPath(), timetableLecture) .currentDate(Timetable::updatedAt.toDotPath()), ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() - override suspend fun pullLecture(timeTableId: String, timetableLectureId: String): Timetable = + override suspend fun pullTimetableLecture(timeTableId: String, timetableLectureId: String): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( Update().pull( Timetable::lectures.toDotPath(), @@ -82,7 +83,15 @@ class TimetableCustomRepositoryImpl( ).currentDate(Timetable::updatedAt.toDotPath()), ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() - override suspend fun pullLectures(timeTableId: String, timetableLectureIds: List): Timetable = + override suspend fun pullTimetableLectureByLectureId(timeTableId: String, lectureId: String): Timetable = + reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( + Update().pull( + Timetable::lectures.toDotPath(), + Query.query(TimetableLecture::lectureId isEqualTo lectureId) + ).currentDate(Timetable::updatedAt.toDotPath()), + ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() + + override suspend fun pullTimetableLectures(timeTableId: String, timetableLectureIds: List): Timetable = reactiveMongoTemplate.update().matching(Timetable::id isEqualTo timeTableId).apply( Update().pull( Timetable::lectures.toDotPath(), @@ -90,7 +99,7 @@ class TimetableCustomRepositoryImpl( ).currentDate(Timetable::updatedAt.toDotPath()), ).withOptions(FindAndModifyOptions.options().returnNew(true)).findModifyAndAwait() - override suspend fun updateLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = + override suspend fun updateTimetableLecture(timeTableId: String, timetableLecture: TimetableLecture): Timetable = reactiveMongoTemplate.update().matching( Timetable::id.isEqualTo(timeTableId).and("lecture_list._id").isEqualTo(ObjectId(timetableLecture.id)) ).apply(Update().apply { set("""lecture_list.$""", timetableLecture) }) diff --git a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt index c1b04342..02387f83 100644 --- a/core/src/main/kotlin/timetables/service/TimetableLectureService.kt +++ b/core/src/main/kotlin/timetables/service/TimetableLectureService.kt @@ -64,7 +64,7 @@ class TimetableLectureServiceImpl( if (timetable.lectures.any { it.lectureId == lectureId }) throw DuplicateTimetableLectureException val timetableLecture = TimetableLecture(lecture, colorIndex, color) resolveTimeConflict(timetable, timetableLecture, isForced) - return timetableRepository.pushLecture(timetable.id!!, timetableLecture) + return timetableRepository.pushTimetableLecture(timetable.id!!, timetableLecture) } override suspend fun addCustomTimetableLecture( @@ -77,7 +77,7 @@ class TimetableLectureServiceImpl( val timetableLecture = timetableLectureRequest.toTimetableLecture() if (ClassTimeUtils.timesOverlap(timetableLecture.classPlaceAndTimes)) throw InvalidTimeException resolveTimeConflict(timetable, timetableLecture, isForced) - return timetableRepository.pushLecture(timetable.id!!, timetableLecture) + return timetableRepository.pushTimetableLecture(timetable.id!!, timetableLecture) } override suspend fun resetTimetableLecture( @@ -100,7 +100,7 @@ class TimetableLectureServiceImpl( classPlaceAndTimes = originalLecture.classPlaceAndTimes } resolveTimeConflict(timetable, timetableLecture, isForced) - return timetableRepository.updateLecture(timetableId, timetableLecture) + return timetableRepository.updateTimetableLecture(timetableId, timetableLecture) } override suspend fun modifyTimetableLecture( @@ -126,12 +126,12 @@ class TimetableLectureServiceImpl( classPlaceAndTimes = modifyTimetableLectureRequestDto.classPlaceAndTimes?.map { it.toClassPlaceAndTime() } ?: classPlaceAndTimes } resolveTimeConflict(timetable, timetableLecture, isForced) - return timetableRepository.updateLecture(timetableId, timetableLecture) + return timetableRepository.updateTimetableLecture(timetableId, timetableLecture) } override suspend fun deleteTimetableLecture(userId: String, timetableId: String, timetableLectureId: String): Timetable { timetableRepository.findByUserIdAndId(userId, timetableId) ?: throw TimetableNotFoundException - return timetableRepository.pullLecture(timetableId, timetableLectureId) + return timetableRepository.pullTimetableLecture(timetableId, timetableLectureId) } private suspend fun resolveTimeConflict( @@ -152,7 +152,7 @@ class TimetableLectureServiceImpl( } overlappingLectures.isNotEmpty() && isForced -> { - timetableRepository.pullLectures(timetable.id!!, overlappingLectures.map { it.id }) + timetableRepository.pullTimetableLectures(timetable.id!!, overlappingLectures.map { it.id }) } } } From 8e6b5d59da65ad74c4c0ee34ee9e9d6a7aec8697 Mon Sep 17 00:00:00 2001 From: Hankyeol Choi Date: Thu, 18 Jan 2024 17:03:17 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=91=9C=20api=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80=20(#210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/handler/TimetableLectureHandler.kt | 8 +- .../main/kotlin/router/docs/TimetableDocs.kt | 261 +++++++++++++++++- 2 files changed, 262 insertions(+), 7 deletions(-) diff --git a/api/src/main/kotlin/handler/TimetableLectureHandler.kt b/api/src/main/kotlin/handler/TimetableLectureHandler.kt index 0091bee5..bd80d77f 100644 --- a/api/src/main/kotlin/handler/TimetableLectureHandler.kt +++ b/api/src/main/kotlin/handler/TimetableLectureHandler.kt @@ -23,7 +23,7 @@ class TimetableLectureHandler( val userId = req.userId val timetableId = req.pathVariable("timetableId") val customTimetable = req.awaitBody() - val isForced = customTimetable.isForced + val isForced = req.parseQueryParam("isForced") ?: customTimetable.isForced timetableLectureService.addCustomTimetableLecture( userId = userId, @@ -37,7 +37,7 @@ class TimetableLectureHandler( val userId = req.userId val timetableId = req.pathVariable("timetableId") val lectureId = req.pathVariable("lectureId") - val isForced = req.awaitBodyOrNull()?.isForced ?: false + val isForced = req.parseQueryParam("isForced") ?: req.awaitBodyOrNull()?.isForced ?: false timetableLectureService.addLecture( userId = userId, @@ -51,7 +51,7 @@ class TimetableLectureHandler( val userId = req.userId val timetableId = req.pathVariable("timetableId") val timetableLectureId = req.pathVariable("timetableLectureId") - val isForced = req.awaitBodyOrNull()?.isForced ?: false + val isForced = req.parseQueryParam("isForced") ?: req.awaitBodyOrNull()?.isForced ?: false timetableLectureService.resetTimetableLecture( userId = userId, @@ -66,7 +66,7 @@ class TimetableLectureHandler( val timetableId = req.pathVariable("timetableId") val timetableLectureId = req.pathVariable("timetableLectureId") val modifyRequestDto = req.awaitBody() - val isForced = modifyRequestDto.isForced + val isForced = req.parseQueryParam("isForced") ?: modifyRequestDto.isForced timetableLectureService.modifyTimetableLecture( userId = userId, diff --git a/api/src/main/kotlin/router/docs/TimetableDocs.kt b/api/src/main/kotlin/router/docs/TimetableDocs.kt index 9b6687df..5388ba8f 100644 --- a/api/src/main/kotlin/router/docs/TimetableDocs.kt +++ b/api/src/main/kotlin/router/docs/TimetableDocs.kt @@ -1,8 +1,18 @@ package com.wafflestudio.snu4t.router.docs +import com.wafflestudio.snu4t.timetables.dto.TimetableLegacyDto +import com.wafflestudio.snu4t.timetables.dto.request.CustomTimetableLectureAddLegacyRequestDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableAddRequestDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableLectureModifyLegacyRequestDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableModifyRequestDto +import com.wafflestudio.snu4t.timetables.dto.request.TimetableModifyThemeRequestDto import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.ParameterIn +import io.swagger.v3.oas.annotations.media.ArraySchema import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody import io.swagger.v3.oas.annotations.responses.ApiResponse import org.springdoc.core.annotations.RouterOperation import org.springdoc.core.annotations.RouterOperations @@ -15,14 +25,259 @@ import timetables.dto.TimetableBriefDto path = "/v1/tables", method = [RequestMethod.GET], produces = [MediaType.APPLICATION_JSON_VALUE], operation = Operation( operationId = "getBrief", - responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableBriefDto::class))])] + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableBriefDto::class)))] + ) + ] ), ), RouterOperation( - path = "/v1/tables/{id}/primary", method = [RequestMethod.POST], produces = [MediaType.APPLICATION_JSON_VALUE], + path = "/v1/tables/recent", + method = [RequestMethod.GET], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "getMostRecentlyUpdatedTimetables", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{year}/{semester}", + method = [RequestMethod.GET], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "getTimetablesBySemester", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableLegacyDto::class)))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables", + method = [RequestMethod.POST], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "addTimetable", + requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableAddRequestDto::class))]), + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableBriefDto::class)))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}", + method = [RequestMethod.GET], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "getTimetable", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}", + method = [RequestMethod.PUT], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "modifyTimetable", + requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableModifyRequestDto::class))]), + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableBriefDto::class)))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}", + method = [RequestMethod.DELETE], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "deleteTimetable", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableBriefDto::class)))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/copy", + method = [RequestMethod.POST], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "copyTimetable", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(array = ArraySchema(schema = Schema(implementation = TimetableBriefDto::class)))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/theme", + method = [RequestMethod.PUT], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "modifyTimetableTheme", + requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableModifyThemeRequestDto::class))]), + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/primary", + method = [RequestMethod.POST], + produces = [MediaType.APPLICATION_JSON_VALUE], operation = Operation( operationId = "setPrimary", - responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = Unit::class))])] + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = Unit::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/primary", + method = [RequestMethod.DELETE], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "unSetPrimary", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = Unit::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/lecture", + method = [RequestMethod.POST], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "addCustomLecture", + parameters = [ + Parameter( + `in` = ParameterIn.QUERY, + name = "isForced", + required = false, + description = "시간 겹치는 강의 강제로 삭제 후 실행" + ), + ], + requestBody = RequestBody(content = [Content(schema = Schema(implementation = CustomTimetableLectureAddLegacyRequestDto::class))]), + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/lecture/{lectureId}", + method = [RequestMethod.POST], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "addLecture", + parameters = [ + Parameter( + `in` = ParameterIn.QUERY, + name = "isForced", + required = false, + description = "시간 겹치는 강의 강제로 삭제 후 실행" + ), + ], + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/lecture/{timetableLectureId}/reset", + method = [RequestMethod.PUT], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "resetTimetableLecture", + parameters = [ + Parameter( + `in` = ParameterIn.QUERY, + name = "isForced", + required = false, + description = "시간 겹치는 강의 강제로 삭제 후 실행" + ), + ], + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/lecture/{timetableLectureId}", + method = [RequestMethod.PUT], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "modifyTimetableLecture", + parameters = [ + Parameter( + `in` = ParameterIn.QUERY, + name = "isForced", + required = false, + description = "시간 겹치는 강의 강제로 삭제 후 실행" + ), + ], + requestBody = RequestBody(content = [Content(schema = Schema(implementation = TimetableLectureModifyLegacyRequestDto::class))]), + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] + ), + ), + RouterOperation( + path = "/v1/tables/{timetableId}/lecture/{timetableLectureId}", + method = [RequestMethod.DELETE], + produces = [MediaType.APPLICATION_JSON_VALUE], + operation = Operation( + operationId = "deleteTimetableLecture", + responses = [ + ApiResponse( + responseCode = "200", + content = [Content(schema = Schema(implementation = TimetableLegacyDto::class))] + ) + ] ), ), )