Skip to content

Commit

Permalink
커스텀 테마 추가 및 누락 스펙 구현 (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
davin111 authored Jan 19, 2024
1 parent 8e6b5d5 commit d089add
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 24 deletions.
7 changes: 7 additions & 0 deletions api/src/main/kotlin/handler/TimetableThemeHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,11 @@ class TimetableThemeHandler(

timetableThemeService.unsetDefault(userId, themeId).let(::TimetableThemeDto)
}

suspend fun unsetBasicThemeTypeDefault(req: ServerRequest) = handle(req) {
val userId = req.userId
val basicThemeType = req.pathVariable("basicThemeTypeValue").toIntOrNull()?.let { BasicThemeType.from(it) } ?: throw InvalidPathParameterException("basicThemeTypeValue")

timetableThemeService.unsetDefault(userId, basicThemeType = basicThemeType).let(::TimetableThemeDto)
}
}
1 change: 1 addition & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class MainRouter(
POST("{themeId}/default", timetableThemeHandler::setDefault)
POST("basic/{basicThemeTypeValue}/default", timetableThemeHandler::setBasicThemeTypeDefault)
DELETE("{themeId}/default", timetableThemeHandler::unsetDefault)
DELETE("basic/{basicThemeTypeValue}/default", timetableThemeHandler::unsetBasicThemeTypeDefault)
}
}

Expand Down
8 changes: 8 additions & 0 deletions api/src/main/kotlin/router/docs/ThemeDocs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,13 @@ import org.springframework.web.bind.annotation.RequestMethod
responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])],
),
),
RouterOperation(
path = "/v1/themes/basic/{basicThemeTypeValue}/default", method = [RequestMethod.DELETE], produces = [MediaType.APPLICATION_JSON_VALUE],
operation = Operation(
operationId = "unsetBasicThemeTypeDefault",
parameters = [Parameter(`in` = ParameterIn.PATH, name = "basicThemeTypeValue", required = true)],
responses = [ApiResponse(responseCode = "200", content = [Content(schema = Schema(implementation = TimetableThemeDto::class))])],
),
),
)
annotation class ThemeDocs
4 changes: 3 additions & 1 deletion core/src/main/kotlin/common/exception/ErrorType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum class ErrorType(
INVALID_PASSWORD(HttpStatus.FORBIDDEN, 0x3001, "password가 유효하지 않습니다.", "비밀번호는 6~20자로 영문자와 숫자를 모두 포함해야 합니다."),
DUPLICATE_LOCAL_ID(HttpStatus.FORBIDDEN, 0x3002, "localId가 중복되었습니다.", "이미 사용 중인 아이디입니다."),
DUPLICATE_TIMETABLE_TITLE(HttpStatus.FORBIDDEN, 0x3003, "timetable title이 중복되었습니다.", "이미 사용중인 시간표 이름입니다."),
DUPLICATE_LECTURE(HttpStatus.FORBIDDEN, 0x3004, "duplicate lecture"),
DUPLICATE_LECTURE(HttpStatus.FORBIDDEN, 0x3004, "duplicate lecture", "duplicate lecture"),
WRONG_SEMESTER(HttpStatus.FORBIDDEN, 0x300A, "잘못된 학기입니다."),
LECTURE_TIME_OVERLAP(HttpStatus.FORBIDDEN, 0x300C, "강의 시간이 겹칩니다."),
CANNOT_RESET_CUSTOM_LECTURE(HttpStatus.FORBIDDEN, 0x300D, "cannot reset custom lectures"),
Expand All @@ -44,6 +44,8 @@ enum class ErrorType(
TABLE_DELETE_ERROR(HttpStatus.BAD_REQUEST, 40010, "하나 남은 시간표는 삭제할 수 없습니다."),
TIMETABLE_NOT_PRIMARY(HttpStatus.BAD_REQUEST, 40011, "대표 시간표가 아닙니다."),
INVALID_THEME_COLOR_COUNT(HttpStatus.BAD_REQUEST, 40012, "테마의 색상 개수가 적절하지 않습니다.", "테마의 색상 개수가 적절하지 않습니다."),
DEFAULT_THEME_DELETE_ERROR(HttpStatus.BAD_REQUEST, 40013, "default 테마는 삭제할 수 없습니다.", "default 테마는 삭제할 수 없습니다."),
NOT_DEFAULT_THEME_ERROR(HttpStatus.BAD_REQUEST, 40014, "default 테마가 아닙니다.", "default 테마가 아닙니다."),

TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40400, "timetable_id가 유효하지 않습니다", "존재하지 않는 시간표입니다."),
PRIMARY_TIMETABLE_NOT_FOUND(HttpStatus.NOT_FOUND, 40401, "timetable_id가 유효하지 않습니다", "대표 시간표가 존재하지 않습니다."),
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/kotlin/common/exception/Snu4tException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ 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 DefaultThemeDeleteErrorException : Snu4tException(ErrorType.DEFAULT_THEME_DELETE_ERROR)
object NotDefaultThemeErrorException : Snu4tException(ErrorType.NOT_DEFAULT_THEME_ERROR)

object NoUserFcmKeyException : Snu4tException(ErrorType.NO_USER_FCM_KEY)
object InvalidRegistrationForPreviousSemesterCourseException :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ interface TimetableRepository : CoroutineCrudRepository<Timetable, String>, Time
suspend fun findByUserIdAndYearAndSemesterAndIsPrimaryTrue(userId: String, year: Int, semester: Semester): Timetable?
suspend fun existsByUserIdAndYearAndSemesterAndTitle(userId: String, year: Int, semester: Semester, title: String): Boolean
suspend fun countAllByUserId(userId: String): Long
suspend fun findByUserIdAndThemeId(userId: String, themeId: String): List<Timetable>
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.wafflestudio.snu4t.timetables.data.TimetableLecture
import com.wafflestudio.snu4t.timetables.dto.request.CustomTimetableLectureAddLegacyRequestDto
import com.wafflestudio.snu4t.timetables.dto.request.TimetableLectureModifyLegacyRequestDto
import com.wafflestudio.snu4t.timetables.repository.TimetableRepository
import com.wafflestudio.snu4t.timetables.utils.ColorUtils
import org.springframework.stereotype.Service

interface TimetableLectureService {
Expand Down Expand Up @@ -54,12 +53,7 @@ class TimetableLectureServiceImpl(
val lecture = lectureRepository.findById(lectureId) ?: throw LectureNotFoundException
if (!(timetable.year == lecture.year && timetable.semester == lecture.semester)) throw WrongSemesterException

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()
}
val (colorIndex, color) = timetableThemeService.getNewColorIndexAndColor(timetable)

if (timetable.lectures.any { it.lectureId == lectureId }) throw DuplicateTimetableLectureException
val timetableLecture = TimetableLecture(lecture, colorIndex, color)
Expand Down
70 changes: 65 additions & 5 deletions core/src/main/kotlin/timetables/service/TimetableThemeService.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.wafflestudio.snu4t.timetables.service

import com.wafflestudio.snu4t.common.enum.BasicThemeType
import com.wafflestudio.snu4t.common.exception.DefaultThemeDeleteErrorException
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.NotDefaultThemeErrorException
import com.wafflestudio.snu4t.common.exception.ThemeNotFoundException
import com.wafflestudio.snu4t.timetables.data.ColorSet
import com.wafflestudio.snu4t.timetables.data.Timetable
import com.wafflestudio.snu4t.timetables.data.TimetableTheme
import com.wafflestudio.snu4t.timetables.repository.TimetableRepository
import com.wafflestudio.snu4t.timetables.repository.TimetableThemeRepository
import kotlinx.coroutines.flow.collect
import org.springframework.stereotype.Service
import java.time.LocalDateTime

Expand All @@ -24,16 +29,19 @@ interface TimetableThemeService {

suspend fun setDefault(userId: String, themeId: String? = null, basicThemeType: BasicThemeType? = null): TimetableTheme

suspend fun unsetDefault(userId: String, themeId: String): TimetableTheme
suspend fun unsetDefault(userId: String, themeId: String? = null, basicThemeType: BasicThemeType? = null): TimetableTheme

suspend fun getDefaultTheme(userId: String): TimetableTheme?

suspend fun getTheme(userId: String, themeId: String? = null, basicThemeType: BasicThemeType? = null): TimetableTheme

suspend fun getNewColorIndexAndColor(timetable: Timetable): Pair<Int, ColorSet?>
}

@Service
class TimetableThemeServiceImpl(
private val timetableThemeRepository: TimetableThemeRepository,
private val timetableRepository: TimetableRepository,
) : TimetableThemeService {
companion object {
private const val MAX_COLOR_COUNT = 9
Expand Down Expand Up @@ -71,13 +79,40 @@ class TimetableThemeServiceImpl(
if (theme.name != it && timetableThemeRepository.existsByUserIdAndName(userId, it)) throw DuplicateThemeNameException
theme.name = it
}
colors?.let { theme.colors = it }

colors?.let {
if (it.size !in 1..MAX_COLOR_COUNT) throw InvalidThemeColorCountException

val colorMap = requireNotNull(theme.colors).mapIndexed { i, color -> color to colors[i] }.toMap()

val timetables = timetableRepository.findByUserIdAndThemeId(userId, themeId)
timetables.forEach { timetable ->
timetable.lectures.forEach { lecture ->
if (lecture.color in theme.colors!!) {
colorMap[lecture.color]?.let { newColor -> lecture.color = newColor }
}
}
}
timetableRepository.saveAll(timetables).collect()

theme.colors = it
}
theme.updatedAt = LocalDateTime.now()
return timetableThemeRepository.save(theme)
}

override suspend fun deleteTheme(userId: String, themeId: String) {
val theme = getCustomTheme(userId, themeId)
if (theme.isDefault) throw DefaultThemeDeleteErrorException

val timetables = timetableRepository.findByUserIdAndThemeId(userId, themeId)

timetables.forEach {
it.theme = BasicThemeType.SNUTT
it.themeId = null
}
timetableRepository.saveAll(timetables).collect()

timetableThemeRepository.delete(theme)
}

Expand Down Expand Up @@ -126,9 +161,16 @@ class TimetableThemeServiceImpl(
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
override suspend fun unsetDefault(userId: String, themeId: String?, basicThemeType: BasicThemeType?): TimetableTheme {
require((themeId == null) xor (basicThemeType == null))

val theme = getDefaultTheme(userId) ?: throw NotDefaultThemeErrorException

themeId?.let {
if (theme.isCustom.not() || theme.id != it) throw NotDefaultThemeErrorException
} ?: run {
if (theme.isCustom || theme.name != basicThemeType!!.displayName) throw NotDefaultThemeErrorException
}

if (theme.isCustom) {
theme.isDefault = false
Expand Down Expand Up @@ -169,6 +211,24 @@ class TimetableThemeServiceImpl(
isCustom = false,
isDefault = isDefault,
)

override suspend fun getNewColorIndexAndColor(timetable: Timetable): Pair<Int, ColorSet?> {
return if (timetable.themeId == null) {
val alreadyUsedIndexes = timetable.lectures.map { it.colorIndex }
val indexToCount = (1..MAX_COLOR_COUNT).associateWith { colorIndex -> alreadyUsedIndexes.count { it == colorIndex } }

val minCount = indexToCount.minOf { it.value }
indexToCount.entries.filter { (_, count) -> count == minCount }.map { it.key }.random() to null
} else {
val theme = getTheme(timetable.userId, timetable.themeId)

val alreadyUsedColors = timetable.lectures.map { requireNotNull(it.color) }
val colorToCount = requireNotNull(theme.colors).associateWith { color -> alreadyUsedColors.count { it == color } }

val minCount = colorToCount.minOf { it.value }
0 to colorToCount.entries.filter { (_, count) -> count == minCount }.map { it.key }.random()
}
}
}

fun TimetableTheme?.toBasicThemeType() = if (this == null || isCustom) BasicThemeType.SNUTT else requireNotNull(BasicThemeType.from(name))
11 changes: 0 additions & 11 deletions core/src/main/kotlin/timetables/utils/ColorUtils.kt

This file was deleted.

0 comments on commit d089add

Please sign in to comment.