From 7b8ba2640d1479fc2ac6f852c125779dcf755af4 Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 28 Oct 2024 21:52:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=B0=EA=B2=BD=20=EA=B5=AC=EB=A7=A4?= =?UTF-8?q?,=20=EC=A1=B0=ED=9A=8C,=20=EC=88=98=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gitanimals/render/app/UserFacade.kt | 19 ++++++ .../render/controller/BackgroundController.kt | 46 +++++++++++++ .../controller/request/ChangeFieldRequest.kt | 5 ++ .../controller/response/BackgroundResponse.kt | 24 +++++++ .../org/gitanimals/render/domain/Field.kt | 65 +++++++++++++++++++ .../org/gitanimals/render/domain/User.kt | 59 +++++++++++++++-- .../gitanimals/render/domain/UserService.kt | 41 ++++++++++-- 7 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/render/controller/BackgroundController.kt create mode 100644 src/main/kotlin/org/gitanimals/render/controller/request/ChangeFieldRequest.kt create mode 100644 src/main/kotlin/org/gitanimals/render/controller/response/BackgroundResponse.kt create mode 100644 src/main/kotlin/org/gitanimals/render/domain/Field.kt diff --git a/src/main/kotlin/org/gitanimals/render/app/UserFacade.kt b/src/main/kotlin/org/gitanimals/render/app/UserFacade.kt index a8edd88..e049c06 100644 --- a/src/main/kotlin/org/gitanimals/render/app/UserFacade.kt +++ b/src/main/kotlin/org/gitanimals/render/app/UserFacade.kt @@ -1,6 +1,7 @@ package org.gitanimals.render.app import org.gitanimals.render.app.request.MergePersonaRequest +import org.gitanimals.render.domain.FieldType import org.gitanimals.render.domain.UserService import org.gitanimals.render.domain.request.PersonaChangeRequest import org.gitanimals.render.domain.response.PersonaResponse @@ -51,4 +52,22 @@ class UserFacade( request.deletePersonaId.toLong(), ) } + + fun addField(token: String, fieldType: FieldType) { + val user = identityApi.getUserByToken(token) + + return userService.addField(user.username, fieldType) + } + + fun deleteField(token: String, fieldType: FieldType) { + val user = identityApi.getUserByToken(token) + + return userService.deleteField(user.username, fieldType) + } + + fun changeField(token: String, fieldType: FieldType) { + val user = identityApi.getUserByToken(token) + + return userService.changeField(user.username, fieldType) + } } diff --git a/src/main/kotlin/org/gitanimals/render/controller/BackgroundController.kt b/src/main/kotlin/org/gitanimals/render/controller/BackgroundController.kt new file mode 100644 index 0000000..e8e5f29 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/controller/BackgroundController.kt @@ -0,0 +1,46 @@ +package org.gitanimals.render.controller + +import org.gitanimals.render.app.UserFacade +import org.gitanimals.render.controller.request.ChangeFieldRequest +import org.gitanimals.render.controller.response.BackgroundResponse +import org.gitanimals.render.domain.FieldType +import org.gitanimals.render.domain.UserService +import org.gitanimals.render.domain.UserService.Companion.loadField +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +class BackgroundController( + private val userFacade: UserFacade, + private val userService: UserService, +) { + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/users/{username}/backgrounds") + fun getBackgrounds( + @PathVariable("username") username: String, + ) = BackgroundResponse.from(userService.getByNameWithLazyLoading(username, loadField)) + + @ResponseStatus(HttpStatus.OK) + @PutMapping("/users/backgrounds") + fun changeBackground( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestBody changeFieldRequest: ChangeFieldRequest, + ) = userFacade.changeField(token, FieldType.valueOf(changeFieldRequest.type.uppercase())) + + @ResponseStatus(HttpStatus.OK) + @PostMapping("/internals/backgrounds") + fun addBackground( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestParam(name = "name") name: String, + ) = userFacade.addField(token, FieldType.valueOf(name.uppercase())) + + + @ResponseStatus(HttpStatus.OK) + @DeleteMapping("/internals/backgrounds") + fun deleteBackground( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestParam(name = "name") name: String, + ) = userFacade.deleteField(token, FieldType.valueOf(name.uppercase())) +} diff --git a/src/main/kotlin/org/gitanimals/render/controller/request/ChangeFieldRequest.kt b/src/main/kotlin/org/gitanimals/render/controller/request/ChangeFieldRequest.kt new file mode 100644 index 0000000..53bc065 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/controller/request/ChangeFieldRequest.kt @@ -0,0 +1,5 @@ +package org.gitanimals.render.controller.request + +data class ChangeFieldRequest( + val type: String, +) diff --git a/src/main/kotlin/org/gitanimals/render/controller/response/BackgroundResponse.kt b/src/main/kotlin/org/gitanimals/render/controller/response/BackgroundResponse.kt new file mode 100644 index 0000000..4a44730 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/controller/response/BackgroundResponse.kt @@ -0,0 +1,24 @@ +package org.gitanimals.render.controller.response + +import org.gitanimals.render.domain.User + +data class BackgroundResponse( + val id: String, + val name: String, + val backgrounds: List, +) { + + data class Background( + val type: String, + ) + + companion object { + fun from(user: User): BackgroundResponse { + return BackgroundResponse( + id = user.id.toString(), + name = user.name, + backgrounds = user.fields.map { Background(it.fieldType.toString()) }, + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/render/domain/Field.kt b/src/main/kotlin/org/gitanimals/render/domain/Field.kt new file mode 100644 index 0000000..2ffc24f --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/domain/Field.kt @@ -0,0 +1,65 @@ +package org.gitanimals.render.domain + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.persistence.* +import org.gitanimals.render.core.IdGenerator + +@Entity +@Table(name = "field") +class Field( + @Id + @Column(name = "id") + private val id: Long, + + @Column(name = "field_type") + @Enumerated(value = EnumType.STRING) + val fieldType: FieldType, + + @Column(name = "is_choose", nullable = false) + private var isChoose: Boolean, + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + val user: User, +) { + + fun isChoose(): Boolean = this.isChoose + + fun choose() { + this.isChoose = true + } + + fun unChoose() { + this.isChoose = false + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Field) return false + + return fieldType == other.fieldType + } + + override fun hashCode(): Int { + return fieldType.hashCode() + } + + fun fillBackground(): String = this.fieldType.fillBackground() + + fun loadComponent(name: String, totalCount: Long): String = + this.fieldType.loadComponent(name, totalCount) + + fun drawBorder(): String = this.fieldType.drawBorder() + + companion object { + fun from(user: User, fieldType: FieldType): Field { + return Field( + id = IdGenerator.generate(), + fieldType = fieldType, + isChoose = false, + user = user, + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/render/domain/User.kt b/src/main/kotlin/org/gitanimals/render/domain/User.kt index bedacb6..ada7b19 100644 --- a/src/main/kotlin/org/gitanimals/render/domain/User.kt +++ b/src/main/kotlin/org/gitanimals/render/domain/User.kt @@ -41,9 +41,13 @@ class User( @Column(name = "visit", nullable = false) private var visit: Long, - @Enumerated(EnumType.STRING) - @Column(name = "field_type", nullable = false) - val field: FieldType, + @OneToMany( + mappedBy = "user", + fetch = FetchType.LAZY, + cascade = [CascadeType.ALL], + orphanRemoval = true + ) + val fields: MutableSet = mutableSetOf(), @Column(name = "last_persona_give_point", nullable = false) private var lastPersonaGivePoint: Int, @@ -163,7 +167,7 @@ class User( fun createLineAnimation(personaId: Long, mode: Mode): String { val builder = StringBuilder().openLine() - val persona = personas.find { it.id!! >= personaId } + val persona = personas.find { it.id >= personaId } ?: throw IllegalArgumentException("Cannot find persona by id \"$personaId\"") builder.append(persona.toSvgForce(mode)) @@ -175,6 +179,8 @@ class User( } fun createFarmAnimation(): String { + val field = getOrCreateDefaultFieldIfAbsent() + val builder = StringBuilder().openFarm() .append(field.fillBackground()) @@ -186,8 +192,44 @@ class User( .closeSvg() } + fun contributionCount(): Long = contributions.totalCount() + fun changeField(fieldType: FieldType) { + getOrCreateDefaultFieldIfAbsent() + + unChooseField() + chooseField(fieldType) + } + + private fun unChooseField() { + getOrCreateDefaultFieldIfAbsent() + + fields.first { it.isChoose() }.unChoose() + } + + fun addField(fieldType: FieldType) { + getOrCreateDefaultFieldIfAbsent() + + this.fields.add(Field.from(this, fieldType)) + } + + private fun getOrCreateDefaultFieldIfAbsent() = fields.firstOrNull { it.isChoose() } ?: run { + this.addField(FieldType.WHITE_FIELD) + this.chooseField(FieldType.WHITE_FIELD) + + fields.first { it.fieldType == FieldType.WHITE_FIELD } + } + + private fun chooseField(fieldType: FieldType) { + this.fields.first { it.fieldType == fieldType }.choose() + } + + fun deleteField(fieldType: FieldType) { + fields.firstOrNull { it.fieldType == fieldType } + ?.let { fields.remove(it) } + } + private fun List.totalCount(): Long { var totalCount = 0L this.forEach { totalCount += it.contribution } @@ -232,11 +274,10 @@ class User( throw IllegalArgumentException("Not supported word contained in \"${name}\"") } - return User( + val user = User( id = IdGenerator.generate(), name = name, personas = createPersonas(contributions), - field = FieldType.WHITE_FIELD, contributions = contributions.map { val year = it.key val contribution = it.value @@ -244,8 +285,12 @@ class User( }.toMutableList(), visit = 1, version = 0, - lastPersonaGivePoint = (totalContributionCount(contributions) % FOR_NEW_PERSONA_COUNT).toInt() + lastPersonaGivePoint = (totalContributionCount(contributions) % FOR_NEW_PERSONA_COUNT).toInt(), ) + + user.addField(FieldType.WHITE_FIELD) + + return user } private fun createPersonas(contributions: Map): MutableList { diff --git a/src/main/kotlin/org/gitanimals/render/domain/UserService.kt b/src/main/kotlin/org/gitanimals/render/domain/UserService.kt index f2d1626..b8bc412 100644 --- a/src/main/kotlin/org/gitanimals/render/domain/UserService.kt +++ b/src/main/kotlin/org/gitanimals/render/domain/UserService.kt @@ -2,6 +2,7 @@ package org.gitanimals.render.domain import org.gitanimals.render.domain.request.PersonaChangeRequest import org.gitanimals.render.domain.response.PersonaResponse +import org.hibernate.Hibernate import org.springframework.data.repository.findByIdOrNull import org.springframework.orm.ObjectOptimisticLockingFailureException import org.springframework.retry.annotation.Retryable @@ -51,7 +52,7 @@ class UserService( fun createNewUser(name: String, contributions: Map): User = userRepository.save(User.newUser(name, contributions)) - @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 100) + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) @Transactional fun givePersonaByCoupon(name: String, persona: String, code: String) { requireIdempotency("$name:$code") @@ -61,7 +62,7 @@ class UserService( user.giveNewPersonaByType(PersonaType.valueOf(persona.uppercase())) } - @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 100) + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) @Transactional fun changePersona(name: String, personChangeRequest: PersonaChangeRequest): PersonaResponse { val user = getUserByName(name) @@ -74,7 +75,7 @@ class UserService( return PersonaResponse.from(changedPersona) } - @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 100) + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) @Transactional fun addPersona( name: String, @@ -98,7 +99,7 @@ class UserService( idempotencyRepository.save(Idempotency(idempotencyKey)) } - @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 100) + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) @Transactional fun deletePersona(name: String, personaId: Long): PersonaResponse { val user = getUserByName(name) @@ -107,7 +108,7 @@ class UserService( } @Transactional - @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 100) + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) fun mergePersona(id: Long, increasePersonaId: Long, deletePersonaId: Long) { val user = userRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot find user by id \"$id\"") @@ -115,10 +116,40 @@ class UserService( user.mergePersona(increasePersonaId, deletePersonaId) } + @Transactional + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) + fun addField(name: String, fieldType: FieldType) { + getUserByName(name).addField(fieldType) + } + + @Transactional + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) + fun deleteField(name: String, fieldType: FieldType) { + getUserByName(name).deleteField(fieldType) + } + + @Transactional + @Retryable(retryFor = [ObjectOptimisticLockingFailureException::class], maxAttempts = 10) + fun changeField(name: String, fieldType: FieldType) { + getUserByName(name).changeField(fieldType) + } + + fun getByNameWithLazyLoading(name: String, vararg lazyLoading: (User) -> Unit): User { + val user = getUserByName(name) + + lazyLoading.forEach { it(user) } + + return user + } + fun getPersona(name: String, personaId: Long): PersonaResponse { return getUserByName(name).personas .find { it.id == personaId } ?.let { PersonaResponse.from(it) } ?: throw IllegalArgumentException("Cannot find matched persona \"$personaId\" by user name \"$name\"") } + + companion object { + val loadField: (User) -> Unit = { Hibernate.initialize(it.fields) } + } }