Skip to content

Commit

Permalink
커스텀 테마 (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
davin111 authored Jan 9, 2024
1 parent 6703290 commit 062c560
Show file tree
Hide file tree
Showing 22 changed files with 575 additions and 58 deletions.
4 changes: 2 additions & 2 deletions api/src/main/kotlin/handler/TimetableHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimetableModifyThemeRequestDto>().theme
val body = req.awaitBody<TimetableModifyThemeRequestDto>()

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) {
Expand Down
74 changes: 74 additions & 0 deletions api/src/main/kotlin/handler/TimetableThemeHandler.kt
Original file line number Diff line number Diff line change
@@ -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<TimetableThemeAddRequestDto>()

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<TimetableThemeModifyRequestDto>()

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)
}
}
18 changes: 18 additions & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down
91 changes: 91 additions & 0 deletions api/src/main/kotlin/router/docs/ThemeDocs.kt
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions core/src/main/kotlin/common/enum/BasicThemeType.kt
Original file line number Diff line number Diff line change
@@ -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<Int, BasicThemeType> {
override fun convert(source: Int): BasicThemeType {
return requireNotNull(BasicThemeType.from(source))
}
}

@Component
@WritingConverter
class BasicThemeTypeWriteConverter : Converter<BasicThemeType, Int> {
override fun convert(source: BasicThemeType): Int = source.value
}
36 changes: 0 additions & 36 deletions core/src/main/kotlin/common/enum/TimetableTheme.kt

This file was deleted.

4 changes: 4 additions & 0 deletions core/src/main/kotlin/common/exception/ErrorType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,22 @@ 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가 유효하지 않습니다", "대표 시간표가 존재하지 않습니다."),
NO_USER_FCM_KEY(HttpStatus.NOT_FOUND, 40402, "유저 FCM 키가 존재하지 않습니다."),
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, "링크 생성 실패", "링크 생성에 실패했습니다. 잠시 후 다시 시도해주세요."),
}
4 changes: 4 additions & 0 deletions core/src/main/kotlin/common/exception/Snu4tException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand All @@ -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)
5 changes: 3 additions & 2 deletions core/src/main/kotlin/timetables/data/Timetable.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,7 +24,8 @@ data class Timetable(
@JsonProperty("lecture_list")
var lectures: List<TimetableLecture> = emptyList(),
var title: String,
var theme: TimetableTheme,
var theme: BasicThemeType,
var themeId: String?,
@Field("is_primary")
var isPrimary: Boolean? = null,
@Field("updated_at")
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/timetables/data/TimetableLecture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -61,5 +61,5 @@ fun TimetableLecture(lecture: Lecture, colorIndex: Int) = TimetableLecture(
courseNumber = lecture.courseNumber,
courseTitle = lecture.courseTitle,
colorIndex = colorIndex,
color = null,
color = color,
)
Loading

0 comments on commit 062c560

Please sign in to comment.