From 7dddbfe51f7d179e58bbfd23068bca096e833ef9 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 11:12:04 +0900 Subject: [PATCH 1/7] =?UTF-8?q?#52=20feat:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doongjun/commitmon/api/MeController.kt | 28 +++++++++++++++++++ .../commitmon/api/data/MeDetailResponse.kt | 28 +++++++++++++++++++ .../doongjun/commitmon/api/data/MeResponse.kt | 23 +++++++++++++++ .../com/doongjun/commitmon/app/UserService.kt | 12 ++++++-- .../doongjun/commitmon/app/data/GetUserDto.kt | 12 ++++++++ .../commitmon/config/security/JwtFilter.kt | 2 ++ .../core/error/GlobalExceptionHandler.kt | 9 +++++- .../core/error/response/ErrorCode.kt | 1 + .../commitmon/extension/SecurityExtension.kt | 5 ++++ 9 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/MeController.kt create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/data/MeDetailResponse.kt create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/data/MeResponse.kt create mode 100644 src/main/kotlin/com/doongjun/commitmon/extension/SecurityExtension.kt diff --git a/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt new file mode 100644 index 0000000..5841e5b --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt @@ -0,0 +1,28 @@ +package com.doongjun.commitmon.api + +import com.doongjun.commitmon.api.data.MeDetailResponse +import com.doongjun.commitmon.api.data.MeResponse +import com.doongjun.commitmon.app.UserFetchType +import com.doongjun.commitmon.app.UserService +import com.doongjun.commitmon.extension.currentUserId +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/me") +class MeController( + private val userService: UserService, +) { + @GetMapping + fun get(): MeResponse = + MeResponse.from( + user = userService.getSimple(currentUserId!!), + ) + + @GetMapping("/detail") + fun getDetail(): MeDetailResponse = + MeDetailResponse.from( + user = userService.get(currentUserId!!, UserFetchType.ALL), + ) +} diff --git a/src/main/kotlin/com/doongjun/commitmon/api/data/MeDetailResponse.kt b/src/main/kotlin/com/doongjun/commitmon/api/data/MeDetailResponse.kt new file mode 100644 index 0000000..3a88437 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/data/MeDetailResponse.kt @@ -0,0 +1,28 @@ +package com.doongjun.commitmon.api.data + +import com.doongjun.commitmon.app.data.GetSimpleUserDto +import com.doongjun.commitmon.app.data.GetUserDto +import com.doongjun.commitmon.domain.Commitmon + +data class MeDetailResponse( + val id: Long, + val name: String, + val totalCommitCount: Long, + val commitmon: Commitmon, + val exp: Int = 0, + val followers: List, + val following: List, +) { + companion object { + fun from(user: GetUserDto): MeDetailResponse = + MeDetailResponse( + id = user.id, + name = user.name, + totalCommitCount = user.totalCommitCount, + commitmon = user.commitmon, + exp = user.exp, + followers = user.followers, + following = user.following, + ) + } +} diff --git a/src/main/kotlin/com/doongjun/commitmon/api/data/MeResponse.kt b/src/main/kotlin/com/doongjun/commitmon/api/data/MeResponse.kt new file mode 100644 index 0000000..9b3f9c5 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/data/MeResponse.kt @@ -0,0 +1,23 @@ +package com.doongjun.commitmon.api.data + +import com.doongjun.commitmon.app.data.GetSimpleUserDto +import com.doongjun.commitmon.domain.Commitmon + +data class MeResponse( + val id: Long, + val name: String, + val totalCommitCount: Long, + val commitmon: Commitmon, + val exp: Int = 0, +) { + companion object { + fun from(user: GetSimpleUserDto): MeResponse = + MeResponse( + id = user.id, + name = user.name, + totalCommitCount = user.totalCommitCount, + commitmon = user.commitmon, + exp = user.exp, + ) + } +} diff --git a/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt b/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt index f007bde..0ebe53e 100644 --- a/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt +++ b/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt @@ -1,6 +1,7 @@ package com.doongjun.commitmon.app import com.doongjun.commitmon.app.data.CreateUserDto +import com.doongjun.commitmon.app.data.GetSimpleUserDto import com.doongjun.commitmon.app.data.GetUserDto import com.doongjun.commitmon.app.data.UpdateUserDto import com.doongjun.commitmon.domain.User @@ -18,13 +19,18 @@ class UserService( @Transactional(readOnly = true) fun existsByName(name: String) = userRepository.existsByName(name) + @Transactional(readOnly = true) + fun getSimple(id: Long): GetSimpleUserDto = + userRepository.findByIdOrNull(id)?.let { user -> GetSimpleUserDto.from(user) } + ?: throw NoSuchElementException("Failed to fetch user by id: $id") + @Transactional(readOnly = true) fun get( id: Long, userFetchType: UserFetchType, ): GetUserDto = userRepository.findByIdOrNull(id)?.let { user -> GetUserDto.from(user, userFetchType) } - ?: throw IllegalArgumentException("Failed to fetch user by id: $id") + ?: throw NoSuchElementException("Failed to fetch user by id: $id") @Cacheable(value = ["userInfo"], key = "#name + '-' + #userFetchType.title") @Transactional(readOnly = true) @@ -33,7 +39,7 @@ class UserService( userFetchType: UserFetchType, ): GetUserDto = userRepository.findByName(name)?.let { user -> GetUserDto.from(user, userFetchType) } - ?: throw IllegalArgumentException("Failed to fetch user by name: $name") + ?: throw NoSuchElementException("Failed to fetch user by name: $name") fun create(dto: CreateUserDto): Long { val user = @@ -54,7 +60,7 @@ class UserService( ) { val user = userRepository.findByIdOrNull(id) - ?: throw IllegalArgumentException("Failed to fetch user by id: $id") + ?: throw NoSuchElementException("Failed to fetch user by id: $id") user.update( totalCommitCount = dto.totalCommitCount, diff --git a/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt b/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt index b1de78a..c4e0d8b 100644 --- a/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt +++ b/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt @@ -13,7 +13,16 @@ data class GetUserDto( val commitmon: Commitmon, val exp: Int = 0, val fetchedUsers: List, + private val followerIds: List, + private val followingIds: List, ) { + val followers: List by lazy { + fetchedUsers.filter { it.id in followerIds } + } + val following: List by lazy { + fetchedUsers.filter { it.id in followingIds } + } + fun toSimple(): GetSimpleUserDto = GetSimpleUserDto( id = id, @@ -39,12 +48,15 @@ data class GetUserDto( UserFetchType.ALL -> listOf(user.followers, user.following) .flatten() + .distinct() .map { GetSimpleUserDto.from(it) } UserFetchType.MUTUAL -> user.mutualFollowers .map { GetSimpleUserDto.from(it) } UserFetchType.SOLO -> emptyList() }, + followerIds = user.followers.map { it.id }, + followingIds = user.following.map { it.id }, ) } } diff --git a/src/main/kotlin/com/doongjun/commitmon/config/security/JwtFilter.kt b/src/main/kotlin/com/doongjun/commitmon/config/security/JwtFilter.kt index 7c8d13a..2fb6a4a 100644 --- a/src/main/kotlin/com/doongjun/commitmon/config/security/JwtFilter.kt +++ b/src/main/kotlin/com/doongjun/commitmon/config/security/JwtFilter.kt @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.http.HttpHeaders import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.security.web.context.RequestAttributeSecurityContextRepository @@ -29,6 +30,7 @@ class JwtFilter( UsernamePasswordAuthenticationToken( userId, null, + listOf(SimpleGrantedAuthority("ROLE_USER")), ) authenticationToken.details = WebAuthenticationDetailsSource().buildDetails(request) diff --git a/src/main/kotlin/com/doongjun/commitmon/core/error/GlobalExceptionHandler.kt b/src/main/kotlin/com/doongjun/commitmon/core/error/GlobalExceptionHandler.kt index 43a4a71..4ddf371 100644 --- a/src/main/kotlin/com/doongjun/commitmon/core/error/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/doongjun/commitmon/core/error/GlobalExceptionHandler.kt @@ -79,7 +79,7 @@ class GlobalExceptionHandler { @ExceptionHandler(AccountExpiredException::class) protected fun handleAccountExpiredException(e: AccountExpiredException): ResponseEntity { - log.error("AccessDeniedException", e) + log.error("AccountExpiredException", e) val response = ErrorResponse.of(ErrorCode.UNAUTHORIZED) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response) } @@ -90,4 +90,11 @@ class GlobalExceptionHandler { val response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE) return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response) } + + @ExceptionHandler(NoSuchElementException::class) + protected fun handleNoSuchElementException(e: NoSuchElementException): ResponseEntity { + log.error("NoSuchElementException", e) + val response = ErrorResponse.of(ErrorCode.NOT_FOUND) + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response) + } } diff --git a/src/main/kotlin/com/doongjun/commitmon/core/error/response/ErrorCode.kt b/src/main/kotlin/com/doongjun/commitmon/core/error/response/ErrorCode.kt index 2fcccbb..03bcada 100644 --- a/src/main/kotlin/com/doongjun/commitmon/core/error/response/ErrorCode.kt +++ b/src/main/kotlin/com/doongjun/commitmon/core/error/response/ErrorCode.kt @@ -8,6 +8,7 @@ enum class ErrorCode( INVALID_INPUT_VALUE(400, "A001", "Invalid input value."), METHOD_NOT_ALLOWED(405, "A002", "Invalid input value."), INTERNAL_SERVER_ERROR(500, "A003", "Server Error."), + NOT_FOUND(404, "A004", "Not Found."), INVALID_TYPE_VALUE(400, "A005", "Invalid type value."), ACCESS_DENIED(403, "A006", "Access is denied."), UNAUTHORIZED(401, "A007", "Unauthorized."), diff --git a/src/main/kotlin/com/doongjun/commitmon/extension/SecurityExtension.kt b/src/main/kotlin/com/doongjun/commitmon/extension/SecurityExtension.kt new file mode 100644 index 0000000..8018bc2 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/extension/SecurityExtension.kt @@ -0,0 +1,5 @@ +package com.doongjun.commitmon.extension + +import org.springframework.security.core.context.SecurityContextHolder + +val currentUserId get() = SecurityContextHolder.getContext().authentication?.principal as Long? From f9d58a64a36c6aa3af8ef1ae5baaf1b5ba3b0937 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 13:24:39 +0900 Subject: [PATCH 2/7] =?UTF-8?q?#52=20feat:=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=EC=97=85=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commitmon/domain/CommitmonLevel.kt | 2 +- .../com/doongjun/commitmon/domain/User.kt | 20 +++- .../doongjun/commitmon/app/UserServiceTest.kt | 26 +++++ .../com/doongjun/commitmon/domain/UserTest.kt | 103 ++++++++++++++++++ src/test/resources/application-test.yml | 12 +- 5 files changed, 159 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/doongjun/commitmon/domain/CommitmonLevel.kt b/src/main/kotlin/com/doongjun/commitmon/domain/CommitmonLevel.kt index 0f07317..c1b0fe9 100644 --- a/src/main/kotlin/com/doongjun/commitmon/domain/CommitmonLevel.kt +++ b/src/main/kotlin/com/doongjun/commitmon/domain/CommitmonLevel.kt @@ -2,7 +2,7 @@ package com.doongjun.commitmon.domain enum class CommitmonLevel( val exp: Long, - private val order: Int, + val order: Int, ) { EGG(0, 0), // 알 BABY(100, 1), // 유아기 diff --git a/src/main/kotlin/com/doongjun/commitmon/domain/User.kt b/src/main/kotlin/com/doongjun/commitmon/domain/User.kt index 5a76865..7c8acec 100644 --- a/src/main/kotlin/com/doongjun/commitmon/domain/User.kt +++ b/src/main/kotlin/com/doongjun/commitmon/domain/User.kt @@ -34,6 +34,9 @@ class User( var commitmon: Commitmon = Commitmon.randomCommitmon(CommitmonLevel.fromExp(totalCommitCount)) protected set + @Column(name = "auto_level_up", nullable = false) + var autoLevelUp: Boolean = true + @OneToMany(mappedBy = "follower", fetch = LAZY, cascade = [ALL], orphanRemoval = true) protected val mutableFollowers: MutableSet = toFollowers(followers).toMutableSet() val followers: List get() = mutableFollowers.map { it.following } @@ -59,12 +62,25 @@ class User( following: List, ) { this.totalCommitCount = totalCommitCount - syncCommitmonLevel(CommitmonLevel.fromExp(totalCommitCount)) updateFollowers(followers) updateFollowing(following) + + if (this.autoLevelUp) { + autoLevelUpCommitmon(CommitmonLevel.fromExp(totalCommitCount)) + } + } + + fun changeCommitmon(commitmon: Commitmon) { + val currentLevel = CommitmonLevel.fromExp(totalCommitCount) + if (commitmon.level.order > currentLevel.order) { + throw IllegalArgumentException("Cannot change to higher level") + } + + this.commitmon = commitmon + this.autoLevelUp = commitmon.level == currentLevel } - private fun syncCommitmonLevel(level: CommitmonLevel) { + private fun autoLevelUpCommitmon(level: CommitmonLevel) { if (this.commitmon.level != level) { this.commitmon = Commitmon.randomLevelTreeCommitmon(level, this.commitmon) } diff --git a/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt b/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt index 6392301..400059f 100644 --- a/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt +++ b/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt @@ -2,6 +2,7 @@ package com.doongjun.commitmon.app import com.doongjun.commitmon.app.data.CreateUserDto import com.doongjun.commitmon.app.data.UpdateUserDto +import com.doongjun.commitmon.domain.Commitmon import com.doongjun.commitmon.domain.CommitmonLevel import com.doongjun.commitmon.domain.User import com.doongjun.commitmon.domain.UserRepository @@ -163,4 +164,29 @@ class UserServiceTest : BaseAppTest() { assertThat(findUser?.followers).containsExactlyInAnyOrder(anotherUser1, anotherUser2) assertThat(findUser?.following).containsExactlyInAnyOrder(anotherUser2, anotherUser3) } + + @Test + fun update_ThenNotBeLeveledUp_Test() { + // given + val name = "doongjun" + val user = userRepository.save(User(name = name, totalCommitCount = 200L)) + user.changeCommitmon(Commitmon.EGG) + val dto = + UpdateUserDto( + totalCommitCount = 1600L, + followerNames = emptyList(), + followingNames = emptyList(), + ) + clear() + + // when + userService.update(user.id, dto) + clear() + + // then + val findUser = userRepository.findByIdOrNull(user.id) + assertThat(findUser?.totalCommitCount).isEqualTo(dto.totalCommitCount) + assertThat(findUser?.commitmon?.level).isEqualTo(CommitmonLevel.EGG) + assertThat(findUser?.autoLevelUp).isFalse() + } } diff --git a/src/test/kotlin/com/doongjun/commitmon/domain/UserTest.kt b/src/test/kotlin/com/doongjun/commitmon/domain/UserTest.kt index 97d198a..f524144 100644 --- a/src/test/kotlin/com/doongjun/commitmon/domain/UserTest.kt +++ b/src/test/kotlin/com/doongjun/commitmon/domain/UserTest.kt @@ -2,6 +2,7 @@ package com.doongjun.commitmon.domain import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class UserTest { private val name = "doongjun" @@ -117,6 +118,108 @@ class UserTest { assertThat(user.mutualFollowers).containsExactlyElementsOf(listOf(mutualFollower)) } + @Test + fun update_ThenBeLeveledUp_Test() { + // given + val user = + User( + name = name, + totalCommitCount = 200L, + ) + user.changeCommitmon(Commitmon.KOROMON) + + val updateTotalCommitCount = 800L + // when + user.update( + totalCommitCount = updateTotalCommitCount, + followers = emptyList(), + following = emptyList(), + ) + + // then + assertThat(user.id).isNotNull() + assertThat(user.totalCommitCount).isEqualTo(updateTotalCommitCount) + assertThat(user.commitmon).isEqualTo(Commitmon.GREYMON) + assertThat(user.commitmon.level).isEqualTo(CommitmonLevel.CHAMPION) + assertThat(user.autoLevelUp).isTrue() + } + + @Test + fun update_ThenNotBeLeveledUp_Test() { + // given + val user = + User( + name = name, + totalCommitCount = 200L, + ) + user.changeCommitmon(Commitmon.BOTAMON) + + val updateTotalCommitCount = 400L + // when + user.update( + totalCommitCount = updateTotalCommitCount, + followers = emptyList(), + following = emptyList(), + ) + + // then + assertThat(user.id).isNotNull() + assertThat(user.totalCommitCount).isEqualTo(updateTotalCommitCount) + assertThat(user.commitmon.level).isEqualTo(CommitmonLevel.BABY) + assertThat(user.autoLevelUp).isFalse() + } + + @Test + fun changeCommitmon_ThenSameLevel_Test() { + // given + val user = + User( + name = name, + totalCommitCount = 200L, + ) + + // when + user.changeCommitmon(Commitmon.KOROMON) + + // then + assertThat(user.commitmon).isEqualTo(Commitmon.KOROMON) + assertThat(user.autoLevelUp).isTrue() + } + + @Test + fun changeCommitmon_ThenLowerLevel_Test() { + // given + val user = + User( + name = name, + totalCommitCount = 200L, + ) + + // when + user.changeCommitmon(Commitmon.EGG) + + // then + assertThat(user.commitmon).isEqualTo(Commitmon.EGG) + assertThat(user.autoLevelUp).isFalse() + } + + @Test + fun changeCommitmon_ThenHigherLevel_Test() { + // given + val user = + User( + name = name, + totalCommitCount = 200L, + ) + + // when, then + val exception = + assertThrows { + user.changeCommitmon(Commitmon.ATLURKABUTERIMON) + } + assertThat(exception.message).isEqualTo("Cannot change to higher level") + } + @Test fun exp_Then0_Test() { // given diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b88725a..e091e44 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -27,6 +27,16 @@ logging: org.hibernate.orm.jdbc.bind: TRACE app: + auth: + jwt: + base64-secret: 12345 + expired-ms: 1000 + refresh-token: + expired-ms: 1000 github: token: 12345 - base-url: https://api.github.com \ No newline at end of file + base-url: https://api.github.com + oauth2: + base-url: https://github.com/login/oauth + client-id: 12345 + client-secret: 12345 \ No newline at end of file From 209c005e285c5f29c1a7949f690356b166b83a99 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 13:36:57 +0900 Subject: [PATCH 3/7] =?UTF-8?q?#52=20feat:=20=EC=BB=A4=EB=B0=8B=EB=AA=AC?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doongjun/commitmon/api/MeController.kt | 20 ++++++- .../api/data/ChangeCommitmonRequest.kt | 9 ++++ .../com/doongjun/commitmon/app/UserService.kt | 14 +++++ .../doongjun/commitmon/app/UserServiceTest.kt | 53 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/data/ChangeCommitmonRequest.kt diff --git a/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt index 5841e5b..e06d468 100644 --- a/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt +++ b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt @@ -1,11 +1,15 @@ package com.doongjun.commitmon.api +import com.doongjun.commitmon.api.data.ChangeCommitmonRequest import com.doongjun.commitmon.api.data.MeDetailResponse import com.doongjun.commitmon.api.data.MeResponse import com.doongjun.commitmon.app.UserFetchType import com.doongjun.commitmon.app.UserService import com.doongjun.commitmon.extension.currentUserId +import jakarta.validation.Valid import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -23,6 +27,20 @@ class MeController( @GetMapping("/detail") fun getDetail(): MeDetailResponse = MeDetailResponse.from( - user = userService.get(currentUserId!!, UserFetchType.ALL), + user = + userService.get( + id = currentUserId!!, + userFetchType = UserFetchType.ALL, + ), ) + + @PostMapping("/commitmon/change") + fun changeCommitmon( + @Valid @RequestBody request: ChangeCommitmonRequest, + ) { + userService.changeCommitmon( + id = currentUserId!!, + commitmon = request.commitmon!!, + ) + } } diff --git a/src/main/kotlin/com/doongjun/commitmon/api/data/ChangeCommitmonRequest.kt b/src/main/kotlin/com/doongjun/commitmon/api/data/ChangeCommitmonRequest.kt new file mode 100644 index 0000000..2117997 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/data/ChangeCommitmonRequest.kt @@ -0,0 +1,9 @@ +package com.doongjun.commitmon.api.data + +import com.doongjun.commitmon.domain.Commitmon +import jakarta.validation.constraints.NotNull + +data class ChangeCommitmonRequest( + @field:NotNull + val commitmon: Commitmon?, +) diff --git a/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt b/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt index 0ebe53e..6dc69d5 100644 --- a/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt +++ b/src/main/kotlin/com/doongjun/commitmon/app/UserService.kt @@ -4,6 +4,7 @@ import com.doongjun.commitmon.app.data.CreateUserDto import com.doongjun.commitmon.app.data.GetSimpleUserDto import com.doongjun.commitmon.app.data.GetUserDto import com.doongjun.commitmon.app.data.UpdateUserDto +import com.doongjun.commitmon.domain.Commitmon import com.doongjun.commitmon.domain.User import com.doongjun.commitmon.domain.UserRepository import org.springframework.cache.annotation.Cacheable @@ -70,4 +71,17 @@ class UserService( userRepository.save(user) } + + fun changeCommitmon( + id: Long, + commitmon: Commitmon, + ) { + val user = + userRepository.findByIdOrNull(id) + ?: throw NoSuchElementException("Failed to fetch user by id: $id") + + user.changeCommitmon(commitmon) + + userRepository.save(user) + } } diff --git a/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt b/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt index 400059f..d879935 100644 --- a/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt +++ b/src/test/kotlin/com/doongjun/commitmon/app/UserServiceTest.kt @@ -8,6 +8,7 @@ import com.doongjun.commitmon.domain.User import com.doongjun.commitmon.domain.UserRepository import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.springframework.beans.factory.annotation.Autowired import org.springframework.data.repository.findByIdOrNull @@ -189,4 +190,56 @@ class UserServiceTest : BaseAppTest() { assertThat(findUser?.commitmon?.level).isEqualTo(CommitmonLevel.EGG) assertThat(findUser?.autoLevelUp).isFalse() } + + @Test + fun changeCommitmon_ThenAutoLevelUpIsTrue_Test() { + // given + val name = "doongjun" + val user = userRepository.save(User(name = name, totalCommitCount = 200L)) + val commitmon = Commitmon.PUKAMON + clear() + + // when + userService.changeCommitmon(user.id, commitmon) + clear() + + // then + val findUser = userRepository.findByIdOrNull(user.id) + assertThat(findUser?.commitmon).isEqualTo(commitmon) + assertThat(findUser?.autoLevelUp).isTrue() + } + + @Test + fun changeCommitmon_ThenAutoLevelUpIsFalse_Test() { + // given + val name = "doongjun" + val user = userRepository.save(User(name = name, totalCommitCount = 200L)) + val commitmon = Commitmon.PICHIMON + clear() + + // when + userService.changeCommitmon(user.id, commitmon) + clear() + + // then + val findUser = userRepository.findByIdOrNull(user.id) + assertThat(findUser?.commitmon).isEqualTo(commitmon) + assertThat(findUser?.autoLevelUp).isFalse() + } + + @Test + fun changeCommitmon_ThenThrowException_Test() { + // given + val name = "doongjun" + val user = userRepository.save(User(name = name, totalCommitCount = 200L)) + val commitmon = Commitmon.WARGREYMON + clear() + + // when, then + val exception = + assertThrows { + userService.changeCommitmon(user.id, commitmon) + } + assertThat(exception.message).isEqualTo("Cannot change to higher level") + } } From a150df90332db1f68ecc139c12e41440104e4365 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 15:40:52 +0900 Subject: [PATCH 4/7] =?UTF-8?q?#52=20feat:=20=EC=BB=A4=EB=B0=8B=EB=AA=AC?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commitmon/api/CommitmonController.kt | 13 ++++++++ .../commitmon/api/data/CommitmonResponse.kt | 33 +++++++++++++++++++ .../config/security/SecurityConfig.kt | 2 ++ .../doongjun/commitmon/domain/Commitmon.kt | 4 +-- 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt create mode 100644 src/main/kotlin/com/doongjun/commitmon/api/data/CommitmonResponse.kt diff --git a/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt b/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt new file mode 100644 index 0000000..0019320 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt @@ -0,0 +1,13 @@ +package com.doongjun.commitmon.api + +import com.doongjun.commitmon.api.data.CommitmonResponse +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/commitmons") +class CommitmonController { + @GetMapping + fun getAll(): CommitmonResponse = CommitmonResponse.of() +} diff --git a/src/main/kotlin/com/doongjun/commitmon/api/data/CommitmonResponse.kt b/src/main/kotlin/com/doongjun/commitmon/api/data/CommitmonResponse.kt new file mode 100644 index 0000000..4d47758 --- /dev/null +++ b/src/main/kotlin/com/doongjun/commitmon/api/data/CommitmonResponse.kt @@ -0,0 +1,33 @@ +package com.doongjun.commitmon.api.data + +import com.doongjun.commitmon.domain.Commitmon +import com.doongjun.commitmon.domain.CommitmonLevel + +data class CommitmonResponse( + val commitmons: List, +) { + data class CommitmonData( + val commitmon: Commitmon, + val name: String, + val nameKo: String, + val level: CommitmonLevel, + val seed: Int, + val imageUrl: String, + ) + + companion object { + private val commitmons = + Commitmon.entries.filter { it != Commitmon.EGG }.map { + CommitmonData( + commitmon = it, + name = it.name.lowercase(), + nameKo = it.nameKo, + level = it.level, + seed = it.seed ?: 0, + imageUrl = "https://s3-commitmon.s3.ap-northeast-2.amazonaws.com/static/${it.assetName}.png", + ) + } + + fun of(): CommitmonResponse = CommitmonResponse(commitmons) + } +} diff --git a/src/main/kotlin/com/doongjun/commitmon/config/security/SecurityConfig.kt b/src/main/kotlin/com/doongjun/commitmon/config/security/SecurityConfig.kt index 0900ce2..edea41c 100644 --- a/src/main/kotlin/com/doongjun/commitmon/config/security/SecurityConfig.kt +++ b/src/main/kotlin/com/doongjun/commitmon/config/security/SecurityConfig.kt @@ -28,6 +28,8 @@ class SecurityConfig( .permitAll() .requestMatchers("/api/v1/account/**") .permitAll() + .requestMatchers("/api/v1/commitmons/**") + .permitAll() .anyRequest() .authenticated() }.exceptionHandling { exception -> diff --git a/src/main/kotlin/com/doongjun/commitmon/domain/Commitmon.kt b/src/main/kotlin/com/doongjun/commitmon/domain/Commitmon.kt index a482a1e..714638c 100644 --- a/src/main/kotlin/com/doongjun/commitmon/domain/Commitmon.kt +++ b/src/main/kotlin/com/doongjun/commitmon/domain/Commitmon.kt @@ -19,7 +19,7 @@ enum class Commitmon( METALGREYMON( CommitmonLevel.PERFECT, listOf(EGG, BOTAMON, KOROMON, AGUMON, GREYMON), - assetName = "metalGreymon", + assetName = "metalgreymon", nameKo = "메탈그레이몬", seed = 1, ), @@ -33,7 +33,7 @@ enum class Commitmon( PUNIMON(CommitmonLevel.BABY, listOf(EGG), assetName = "punimon", nameKo = "푸니몬", seed = 2), TSUNOMON(CommitmonLevel.IN_TRAINING, listOf(EGG, PUNIMON), assetName = "tsunomon", nameKo = "뿔몬", seed = 2), - GABUMON(CommitmonLevel.ROOKIE, listOf(EGG, PUNIMON, TSUNOMON), assetName = "gabumon", nameKo = "가루몬", seed = 2), + GABUMON(CommitmonLevel.ROOKIE, listOf(EGG, PUNIMON, TSUNOMON), assetName = "gabumon", nameKo = "파피몬", seed = 2), GARURUMON(CommitmonLevel.CHAMPION, listOf(EGG, PUNIMON, TSUNOMON, GABUMON), assetName = "garurumon", nameKo = "가루몬", seed = 2), WEREGARURUMON( CommitmonLevel.PERFECT, From 0c660f32df8b63e15c6dcb9fc9c3213a6937b77f Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 15:50:57 +0900 Subject: [PATCH 5/7] =?UTF-8?q?#52=20feat:=20infra=20=EB=A1=9C=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commitmon/infra/GithubGraphqlApi.kt | 24 ++++++++++++++----- .../commitmon/infra/GithubOAuth2Api.kt | 9 +++++-- .../doongjun/commitmon/infra/GithubRestApi.kt | 15 ++++++++---- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/doongjun/commitmon/infra/GithubGraphqlApi.kt b/src/main/kotlin/com/doongjun/commitmon/infra/GithubGraphqlApi.kt index ea84adf..77e0335 100644 --- a/src/main/kotlin/com/doongjun/commitmon/infra/GithubGraphqlApi.kt +++ b/src/main/kotlin/com/doongjun/commitmon/infra/GithubGraphqlApi.kt @@ -6,6 +6,7 @@ import com.doongjun.commitmon.infra.data.UserFollowInfoResponse import com.doongjun.commitmon.infra.data.UserFollowInfoVariables import com.doongjun.commitmon.infra.data.UserFollowersResponse import com.doongjun.commitmon.infra.data.UserFollowingResponse +import org.slf4j.LoggerFactory import org.springframework.core.ParameterizedTypeReference import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Component @@ -16,6 +17,8 @@ import java.nio.charset.Charset class GithubGraphqlApi( private val githubGraphqlWebClient: WebClient, ) { + private val log = LoggerFactory.getLogger(javaClass) + companion object { private val userFollowInfoQuery = ClassPathResource( @@ -53,10 +56,13 @@ class GithubGraphqlApi( .bodyValue(requestBody) .retrieve() .bodyToMono(object : ParameterizedTypeReference>() {}) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch user follow info: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch user follow info: $error") + }.block()!! if (isError(response)) { + log.error("Failed to fetch user follow info: ${response.errors}") throw IllegalArgumentException("Failed to fetch user follow info: ${response.errors}") } @@ -85,10 +91,13 @@ class GithubGraphqlApi( .bodyValue(requestBody) .retrieve() .bodyToMono(object : ParameterizedTypeReference>() {}) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch user followers: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch user followers: $error") + }.block()!! if (isError(response)) { + log.error("Failed to fetch user followers: ${response.errors}") throw IllegalArgumentException("Failed to fetch user followers: ${response.errors}") } @@ -117,10 +126,13 @@ class GithubGraphqlApi( .bodyValue(requestBody) .retrieve() .bodyToMono(object : ParameterizedTypeReference>() {}) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch user following: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch user following: $error") + }.block()!! if (isError(response)) { + log.error("Failed to fetch user following: ${response.errors}") throw IllegalArgumentException("Failed to fetch user following: ${response.errors}") } diff --git a/src/main/kotlin/com/doongjun/commitmon/infra/GithubOAuth2Api.kt b/src/main/kotlin/com/doongjun/commitmon/infra/GithubOAuth2Api.kt index b3439b6..a92794b 100644 --- a/src/main/kotlin/com/doongjun/commitmon/infra/GithubOAuth2Api.kt +++ b/src/main/kotlin/com/doongjun/commitmon/infra/GithubOAuth2Api.kt @@ -1,6 +1,7 @@ package com.doongjun.commitmon.infra import com.doongjun.commitmon.infra.data.OAuthLoginResponse +import org.slf4j.LoggerFactory import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient @@ -8,6 +9,8 @@ import org.springframework.web.reactive.function.client.WebClient class GithubOAuth2Api( private val githubOAuth2WebClient: WebClient, ) { + private val log = LoggerFactory.getLogger(javaClass) + fun fetchAccessToken( code: String, clientId: String, @@ -24,6 +27,8 @@ class GithubOAuth2Api( .build() }.retrieve() .bodyToMono(OAuthLoginResponse::class.java) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch access token: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch access token: $error") + }.block()!! } diff --git a/src/main/kotlin/com/doongjun/commitmon/infra/GithubRestApi.kt b/src/main/kotlin/com/doongjun/commitmon/infra/GithubRestApi.kt index 9d4e688..04b36ed 100644 --- a/src/main/kotlin/com/doongjun/commitmon/infra/GithubRestApi.kt +++ b/src/main/kotlin/com/doongjun/commitmon/infra/GithubRestApi.kt @@ -2,6 +2,7 @@ package com.doongjun.commitmon.infra import com.doongjun.commitmon.infra.data.UserCommitSearchResponse import com.doongjun.commitmon.infra.data.UserInfoResponse +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient @@ -12,6 +13,8 @@ class GithubRestApi( @Value("\${app.github.token}") private val githubToken: String, ) { + private val log = LoggerFactory.getLogger(javaClass) + fun fetchUserInfo(userToken: String): String = githubRestWebClient .get() @@ -23,8 +26,10 @@ class GithubRestApi( headers.add("Authorization", "Bearer $userToken") }.retrieve() .bodyToMono(UserInfoResponse::class.java) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch user: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch user: $error") + }.block()!! .login fun fetchUserCommitSearchInfo(username: String): UserCommitSearchResponse = @@ -40,6 +45,8 @@ class GithubRestApi( headers.add("Authorization", "Bearer $githubToken") }.retrieve() .bodyToMono(UserCommitSearchResponse::class.java) - .onErrorMap { error -> throw IllegalArgumentException("Failed to fetch user commit count: $error") } - .block()!! + .onErrorMap { error -> + log.error(error.message) + throw IllegalArgumentException("Failed to fetch user commit count: $error") + }.block()!! } From 00a5e41c0a593bcbafdbffe3d9bac3e21d130408 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 16:07:12 +0900 Subject: [PATCH 6/7] =?UTF-8?q?#52=20feat:=20Swagger=20=EC=84=A4=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/doongjun/commitmon/api/CommitmonController.kt | 2 ++ src/main/kotlin/com/doongjun/commitmon/api/MeController.kt | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt b/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt index 0019320..188f7f2 100644 --- a/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt +++ b/src/main/kotlin/com/doongjun/commitmon/api/CommitmonController.kt @@ -1,6 +1,7 @@ package com.doongjun.commitmon.api import com.doongjun.commitmon.api.data.CommitmonResponse +import io.swagger.v3.oas.annotations.Operation import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @@ -8,6 +9,7 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v1/commitmons") class CommitmonController { + @Operation(summary = "커밋몬 목록 조회") @GetMapping fun getAll(): CommitmonResponse = CommitmonResponse.of() } diff --git a/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt index e06d468..db4face 100644 --- a/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt +++ b/src/main/kotlin/com/doongjun/commitmon/api/MeController.kt @@ -6,6 +6,7 @@ import com.doongjun.commitmon.api.data.MeResponse import com.doongjun.commitmon.app.UserFetchType import com.doongjun.commitmon.app.UserService import com.doongjun.commitmon.extension.currentUserId +import io.swagger.v3.oas.annotations.Operation import jakarta.validation.Valid import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -18,12 +19,14 @@ import org.springframework.web.bind.annotation.RestController class MeController( private val userService: UserService, ) { + @Operation(summary = "로그인 유저 조회") @GetMapping fun get(): MeResponse = MeResponse.from( user = userService.getSimple(currentUserId!!), ) + @Operation(summary = "로그인 유저 조회 (팔로워, 팔로잉 포함)") @GetMapping("/detail") fun getDetail(): MeDetailResponse = MeDetailResponse.from( @@ -34,6 +37,7 @@ class MeController( ), ) + @Operation(summary = "로그인 유저 커밋몬 변경") @PostMapping("/commitmon/change") fun changeCommitmon( @Valid @RequestBody request: ChangeCommitmonRequest, From 74a6b88999532824bce51cef1dd21ff185ba3f76 Mon Sep 17 00:00:00 2001 From: doongjun Date: Tue, 19 Nov 2024 16:07:33 +0900 Subject: [PATCH 7/7] =?UTF-8?q?#52=20fix:=20setterless=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt b/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt index c4e0d8b..19a987b 100644 --- a/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt +++ b/src/main/kotlin/com/doongjun/commitmon/app/data/GetUserDto.kt @@ -4,8 +4,10 @@ import com.doongjun.commitmon.app.UserFetchType import com.doongjun.commitmon.core.NoArgs import com.doongjun.commitmon.domain.Commitmon import com.doongjun.commitmon.domain.User +import com.fasterxml.jackson.annotation.JsonIgnoreProperties @NoArgs +@JsonIgnoreProperties(value = ["followerIds", "followingIds", "followers", "following"]) data class GetUserDto( val id: Long, val name: String,