From aa11de3a5c91483cf99fa7373434bc07059c660e Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 16 Dec 2024 22:25:34 +0900 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20Guild=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=A0=95=EC=9D=98=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/core/AggregateRoot.kt | 5 ++ .../org/gitanimals/guild/core/IdGenerator.kt | 10 ++++ .../kotlin/org/gitanimals/guild/core/clock.kt | 14 +++++ .../gitanimals/guild/domain/AbstractTime.kt | 32 +++++++++++ .../org/gitanimals/guild/domain/Guild.kt | 56 +++++++++++++++++++ .../guild/domain/GuildRepository.kt | 5 ++ .../org/gitanimals/guild/domain/Member.kt | 46 +++++++++++++++ .../gitanimals/render/domain/AbstractTime.kt | 3 + 8 files changed, 171 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/core/AggregateRoot.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/core/IdGenerator.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/core/clock.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/AbstractTime.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/Guild.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/Member.kt diff --git a/src/main/kotlin/org/gitanimals/guild/core/AggregateRoot.kt b/src/main/kotlin/org/gitanimals/guild/core/AggregateRoot.kt new file mode 100644 index 0000000..8a2cb9f --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/core/AggregateRoot.kt @@ -0,0 +1,5 @@ +package org.gitanimals.guild.core + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class AggregateRoot diff --git a/src/main/kotlin/org/gitanimals/guild/core/IdGenerator.kt b/src/main/kotlin/org/gitanimals/guild/core/IdGenerator.kt new file mode 100644 index 0000000..b9389fd --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/core/IdGenerator.kt @@ -0,0 +1,10 @@ +package org.gitanimals.guild.core + +import com.github.f4b6a3.tsid.TsidFactory + +object IdGenerator { + + private val tsidFactory = TsidFactory.newInstance256() + + fun generate(): Long = tsidFactory.create().toLong() +} diff --git a/src/main/kotlin/org/gitanimals/guild/core/clock.kt b/src/main/kotlin/org/gitanimals/guild/core/clock.kt new file mode 100644 index 0000000..40f89c7 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/core/clock.kt @@ -0,0 +1,14 @@ +package org.gitanimals.guild.core + +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime + +var clock: Clock = Clock.systemUTC() + +fun instant() = Instant.now(clock) + +fun Instant.toZonedDateTime() = ZonedDateTime.ofInstant(this, clock.zone) + +fun Instant.toKr() = ZonedDateTime.ofInstant(this, ZoneId.of("Asia/Seoul")) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/AbstractTime.kt b/src/main/kotlin/org/gitanimals/guild/domain/AbstractTime.kt new file mode 100644 index 0000000..ff15c58 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/AbstractTime.kt @@ -0,0 +1,32 @@ +package org.gitanimals.guild.domain + +import jakarta.persistence.Column +import jakarta.persistence.EntityListeners +import jakarta.persistence.MappedSuperclass +import jakarta.persistence.PrePersist +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.Instant + +@MappedSuperclass +@EntityListeners(AuditingEntityListener::class) +abstract class AbstractTime( + @CreatedDate + @Column(name = "created_at") + val createdAt: Instant = Instant.now(), + + @LastModifiedDate + @Column(name = "modified_at") + var modifiedAt: Instant? = null, +) { + + @PrePersist + fun prePersist() { + modifiedAt = when (modifiedAt == null) { + true -> createdAt + false -> return + } + } +} + diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt new file mode 100644 index 0000000..2186c1c --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -0,0 +1,56 @@ +package org.gitanimals.guild.domain + +import jakarta.persistence.* +import org.gitanimals.guild.core.AggregateRoot +import org.gitanimals.guild.core.IdGenerator + +@Entity +@AggregateRoot +@Table(name = "guild") +class Guild( + @Id + @Column(name = "id") + val id: Long, + + @Column(name = "guild_icon", nullable = false) + val guildIcon: String, + + @Column(name = "title", columnDefinition = "TEXT", nullable = false) + val title: String, + + @Column(name = "body", columnDefinition = "TEXT") + val body: String, + + @Column(name = "leader_id", nullable = false) + val leaderId: Long, + + @OneToMany( + mappedBy = "guild", + orphanRemoval = true, + fetch = FetchType.LAZY, + cascade = [CascadeType.ALL], + ) + private val members: MutableSet, +) : AbstractTime() { + + companion object { + + fun create( + guildIcon: String, + title: String, + body: String, + leaderId: Long, + members: MutableSet = mutableSetOf(), + ): Guild { + + return Guild( + id = IdGenerator.generate(), + guildIcon = guildIcon, + title = title, + body = body, + leaderId = leaderId, + members = members, + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt new file mode 100644 index 0000000..e2d0474 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -0,0 +1,5 @@ +package org.gitanimals.guild.domain + +import org.springframework.data.jpa.repository.JpaRepository + +interface GuildRepository: JpaRepository diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt new file mode 100644 index 0000000..e5765af --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -0,0 +1,46 @@ +package org.gitanimals.guild.domain + +import jakarta.persistence.* +import org.gitanimals.guild.core.IdGenerator + +@Entity +@Table(name = "member") +class Member( + @Id + @Column(name = "id") + val id: Long, + + @Column(name = "user_id", nullable = false) + val userId: Long, + + @Column(name = "persona_id", nullable = false) + val personaId: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "guild_id") + val guild: Guild, +): AbstractTime() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Member) return false + + return userId == other.userId + } + + override fun hashCode(): Int { + return userId.hashCode() + } + + companion object { + + fun create(guild: Guild, userId: Long, personaId: Long): Member { + return Member( + id = IdGenerator.generate(), + userId = userId, + personaId = personaId, + guild = guild, + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/render/domain/AbstractTime.kt b/src/main/kotlin/org/gitanimals/render/domain/AbstractTime.kt index 85f6ec8..40c3be4 100644 --- a/src/main/kotlin/org/gitanimals/render/domain/AbstractTime.kt +++ b/src/main/kotlin/org/gitanimals/render/domain/AbstractTime.kt @@ -1,13 +1,16 @@ package org.gitanimals.render.domain import jakarta.persistence.Column +import jakarta.persistence.EntityListeners import jakarta.persistence.MappedSuperclass import jakarta.persistence.PrePersist import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.time.Instant @MappedSuperclass +@EntityListeners(AuditingEntityListener::class) abstract class AbstractTime( @CreatedDate @Column(name = "created_at") From 6f61ef35d580677e33ccc22d1c1c3cefd3a7a4ba Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 16 Dec 2024 23:29:39 +0900 Subject: [PATCH 02/24] =?UTF-8?q?refactor:=20Guild.Member=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EC=97=90=20name=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/org/gitanimals/guild/domain/Member.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index e5765af..7d82385 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -13,13 +13,16 @@ class Member( @Column(name = "user_id", nullable = false) val userId: Long, + @Column(name = "user_name", nullable = false) + val name: String, + @Column(name = "persona_id", nullable = false) val personaId: Long, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "guild_id") val guild: Guild, -): AbstractTime() { +) : AbstractTime() { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -34,10 +37,11 @@ class Member( companion object { - fun create(guild: Guild, userId: Long, personaId: Long): Member { + fun create(guild: Guild, userId: Long, name: String, personaId: Long): Member { return Member( id = IdGenerator.generate(), userId = userId, + name = name, personaId = personaId, guild = guild, ) From d2e9ee3a1c7ae66416ed79c406fa8de356c526d3 Mon Sep 17 00:00:00 2001 From: devxb Date: Tue, 17 Dec 2024 23:38:49 +0900 Subject: [PATCH 03/24] =?UTF-8?q?refactor:=20Guild=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20farmType=EA=B3=BC=20contributions=EB=A5=BC?= =?UTF-8?q?=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 --- src/main/kotlin/org/gitanimals/guild/domain/Guild.kt | 8 +++++++- .../kotlin/org/gitanimals/guild/domain/GuildFarmType.kt | 4 ++++ src/main/kotlin/org/gitanimals/guild/domain/Member.kt | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 2186c1c..bd05694 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -18,12 +18,16 @@ class Guild( @Column(name = "title", columnDefinition = "TEXT", nullable = false) val title: String, - @Column(name = "body", columnDefinition = "TEXT") + @Column(name = "body", columnDefinition = "TEXT", length = 500) val body: String, @Column(name = "leader_id", nullable = false) val leaderId: Long, + @Enumerated(EnumType.STRING) + @Column(name = "farm_type", nullable = false, columnDefinition = "TEXT") + val farmType: GuildFarmType, + @OneToMany( mappedBy = "guild", orphanRemoval = true, @@ -41,6 +45,7 @@ class Guild( body: String, leaderId: Long, members: MutableSet = mutableSetOf(), + farmType: GuildFarmType, ): Guild { return Guild( @@ -50,6 +55,7 @@ class Guild( body = body, leaderId = leaderId, members = members, + farmType = farmType, ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt new file mode 100644 index 0000000..8332b11 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt @@ -0,0 +1,4 @@ +package org.gitanimals.guild.domain + +enum class GuildFarmType { +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index 7d82385..d497371 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -19,6 +19,9 @@ class Member( @Column(name = "persona_id", nullable = false) val personaId: Long, + @Column(name = "contributions", nullable = false) + private var contributions: Long, + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "guild_id") val guild: Guild, @@ -37,13 +40,14 @@ class Member( companion object { - fun create(guild: Guild, userId: Long, name: String, personaId: Long): Member { + fun create(guild: Guild, userId: Long, name: String, personaId: Long, contributions: Long): Member { return Member( id = IdGenerator.generate(), userId = userId, name = name, personaId = personaId, guild = guild, + contributions = contributions, ) } } From cd60b5cc48c010ea2c85c2c9cd971d7047e2b287 Mon Sep 17 00:00:00 2001 From: devxb Date: Tue, 17 Dec 2024 23:52:44 +0900 Subject: [PATCH 04/24] =?UTF-8?q?refactor:=20Leader=EB=A5=BC=20Embeddeable?= =?UTF-8?q?=EB=A1=9C=20=EB=BA=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gitanimals/guild/domain/Guild.kt | 8 ++++---- .../org/gitanimals/guild/domain/Leader.kt | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/Leader.kt diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index bd05694..3d68bc9 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -21,8 +21,8 @@ class Guild( @Column(name = "body", columnDefinition = "TEXT", length = 500) val body: String, - @Column(name = "leader_id", nullable = false) - val leaderId: Long, + @Embedded + val leader: Leader, @Enumerated(EnumType.STRING) @Column(name = "farm_type", nullable = false, columnDefinition = "TEXT") @@ -43,7 +43,7 @@ class Guild( guildIcon: String, title: String, body: String, - leaderId: Long, + leader: Leader, members: MutableSet = mutableSetOf(), farmType: GuildFarmType, ): Guild { @@ -53,7 +53,7 @@ class Guild( guildIcon = guildIcon, title = title, body = body, - leaderId = leaderId, + leader = leader, members = members, farmType = farmType, ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt new file mode 100644 index 0000000..65a3710 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt @@ -0,0 +1,19 @@ +package org.gitanimals.guild.domain + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +data class Leader( + @Column(name = "leader_id", nullable = false) + val userId: Long, + + @Column(name = "name", nullable = false, columnDefinition = "TEXT") + val name: String, + + @Column(name = "persona_id", nullable = false) + val personaId: Long, + + @Column(name = "contributions", nullable = false) + private var contributions: Long, +) From 9dfc12a006d365c9390f5f1c57380a3b4b2cce52 Mon Sep 17 00:00:00 2001 From: devxb Date: Wed, 18 Dec 2024 23:24:35 +0900 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20Guild=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/CreateGuildFacade.kt | 98 ++++++++++++++ .../org/gitanimals/guild/app/IdentityApi.kt | 40 ++++++ .../org/gitanimals/guild/app/RenderApi.kt | 23 ++++ .../guild/app/request/CreateGuildRequest.kt | 11 ++ .../guild/controller/GuildController.kt | 23 ++++ .../org/gitanimals/guild/domain/Guild.kt | 16 ++- .../gitanimals/guild/domain/GuildFarmType.kt | 3 + .../guild/domain/GuildRepository.kt | 5 +- .../gitanimals/guild/domain/GuildService.kt | 43 ++++++ .../domain/request/CreateLeaderRequest.kt | 20 +++ .../guild/infra/HttpClientConfigurer.kt | 35 +++++ .../controller/response/UserResponse.kt | 2 + .../guild/app/CreateGuildFacadeTest.kt | 123 ++++++++++++++++++ .../guild/app/MockApiConfiguration.kt | 51 ++++++++ .../guild/domain/GuildServiceTest.kt | 77 +++++++++++ .../guild/supports/RedisContainer.kt | 27 ++++ .../gitanimals/guild/supports/SagaCapture.kt | 63 +++++++++ .../gitanimals/render/domain/UserFixture.kt | 2 - 18 files changed, 657 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/app/IdentityApi.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/infra/HttpClientConfigurer.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/supports/RedisContainer.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt new file mode 100644 index 0000000..95c72b1 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt @@ -0,0 +1,98 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.app.request.CreateGuildRequest +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.domain.request.CreateLeaderRequest +import org.rooftop.netx.api.Orchestrator +import org.rooftop.netx.api.OrchestratorFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.util.* + +@Service +class CreateGuildFacade( + private val guildService: GuildService, + private val identityApi: IdentityApi, + private val renderApi: RenderApi, + @Value("\${internal.secret}") internalSecret: String, + orchestratorFactory: OrchestratorFactory, +) { + + private lateinit var createGuildOrchestrator: Orchestrator + + fun createGuild( + token: String, + createGuildRequest: CreateGuildRequest, + ) { + createGuildOrchestrator.sagaSync( + createGuildRequest, + context = mapOf("token" to token, IDEMPOTENCY_KEY to UUID.randomUUID().toString()), + ).decodeResultOrThrow(Unit::class) + } + + init { + createGuildOrchestrator = + orchestratorFactory.create("Create guild orchestrator") + .startWithContext( + contextOrchestrate = { context, createGuildRequest -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext(IDEMPOTENCY_KEY, String::class) + + val leader = identityApi.getUserByToken(token) + require(leader.points.toInt() >= CREATE_GUILD_COST) { + "Cannot create guild cause not enough points. points: \"${leader.points}\"" + } + + identityApi.decreasePoint( + token = token, + internalSecret = internalSecret, + idempotencyKey = idempotencyKey, + point = CREATE_GUILD_COST.toString(), + ) + createGuildRequest + }, + contextRollback = { context, _ -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext(IDEMPOTENCY_KEY, String::class) + + identityApi.increasePoint( + token = token, + internalSecret = internalSecret, + idempotencyKey = idempotencyKey, + point = CREATE_GUILD_COST.toString(), + ) + } + ) + .commitWithContext( + contextOrchestrate = { context, createGuildRequest -> + val token = context.decodeContext("token", String::class) + + val leader = identityApi.getUserByToken(token) + val renderUser = + renderApi.getUserByName(leader.username) + + + val createLeaderRequest = CreateLeaderRequest( + userId = leader.id.toLong(), + name = leader.username, + personaId = renderUser.personas.maxBy { it.level }.id.toLong(), + contributions = renderUser.totalContributions.toLong(), + ) + + guildService.createGuild( + title = createGuildRequest.title, + body = createGuildRequest.body, + guildIcon = createGuildRequest.guildIcon, + farmType = createGuildRequest.farmType, + autoJoin = createGuildRequest.autoJoin, + createLeaderRequest = createLeaderRequest, + ) + } + ) + } + + private companion object { + private const val IDEMPOTENCY_KEY = "IDEMPOTENCY_KEY" + private const val CREATE_GUILD_COST = 30_000 + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/app/IdentityApi.kt b/src/main/kotlin/org/gitanimals/guild/app/IdentityApi.kt new file mode 100644 index 0000000..56f092a --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/IdentityApi.kt @@ -0,0 +1,40 @@ +package org.gitanimals.guild.app + +import org.springframework.http.HttpHeaders +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.service.annotation.GetExchange +import org.springframework.web.service.annotation.PostExchange + +interface IdentityApi { + + @GetExchange("/users") + fun getUserByToken(@RequestHeader(HttpHeaders.AUTHORIZATION) token: String): UserResponse + + @PostExchange("/internals/users/points/decreases") + fun decreasePoint( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestHeader(INTERNAL_SECRET_KEY) internalSecret: String, + @RequestParam("idempotency-key") idempotencyKey: String, + @RequestParam("point") point: String, + ) + + @PostExchange("/internals/users/points/increases") + fun increasePoint( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestHeader(INTERNAL_SECRET_KEY) internalSecret: String, + @RequestParam("idempotency-key") idempotencyKey: String, + @RequestParam("point") point: String, + ) + + data class UserResponse( + val id: String, + val username: String, + val points: String, + val profileImage: String, + ) + + private companion object { + private const val INTERNAL_SECRET_KEY = "Internal-Secret" + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt b/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt new file mode 100644 index 0000000..e254108 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt @@ -0,0 +1,23 @@ +package org.gitanimals.guild.app + +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.service.annotation.GetExchange + +fun interface RenderApi { + + @GetExchange("/users/{username}") + fun getUserByName(@PathVariable("username") username: String): UserResponse + + data class UserResponse( + val id: String, + val name: String, + val totalContributions: String, + val personas: List, + ) { + + data class PersonaResponse( + val id: String, + val level: String, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt b/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt new file mode 100644 index 0000000..e7b604b --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt @@ -0,0 +1,11 @@ +package org.gitanimals.guild.app.request + +import org.gitanimals.guild.domain.GuildFarmType + +data class CreateGuildRequest( + val title: String, + val body: String, + val guildIcon: String, + val autoJoin: Boolean, + val farmType: GuildFarmType, +) diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt new file mode 100644 index 0000000..51233a3 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -0,0 +1,23 @@ +package org.gitanimals.guild.controller + +import org.gitanimals.guild.app.CreateGuildFacade +import org.gitanimals.guild.app.request.CreateGuildRequest +import org.springframework.http.HttpHeaders +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RestController + +@RestController +class GuildController( + private val createGuildFacade: CreateGuildFacade, +) { + + @PostMapping("/guilds") + fun createGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestBody createGuildRequest: CreateGuildRequest, + ) = createGuildFacade.createGuild(token, createGuildRequest) + + +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 3d68bc9..1cf2dd5 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -6,7 +6,12 @@ import org.gitanimals.guild.core.IdGenerator @Entity @AggregateRoot -@Table(name = "guild") +@Table( + name = "guild", + indexes = [ + Index(name = "guild_idx_title", unique = true, columnList = "title") + ] +) class Guild( @Id @Column(name = "id") @@ -15,7 +20,7 @@ class Guild( @Column(name = "guild_icon", nullable = false) val guildIcon: String, - @Column(name = "title", columnDefinition = "TEXT", nullable = false) + @Column(name = "title", unique = true, nullable = false, length = 50) val title: String, @Column(name = "body", columnDefinition = "TEXT", length = 500) @@ -35,6 +40,11 @@ class Guild( cascade = [CascadeType.ALL], ) private val members: MutableSet, + + private var autoJoin: Boolean, + + @Version + private var version: Long? = null, ) : AbstractTime() { companion object { @@ -46,6 +56,7 @@ class Guild( leader: Leader, members: MutableSet = mutableSetOf(), farmType: GuildFarmType, + autoJoin: Boolean, ): Guild { return Guild( @@ -56,6 +67,7 @@ class Guild( leader = leader, members = members, farmType = farmType, + autoJoin = autoJoin, ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt index 8332b11..85c494a 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildFarmType.kt @@ -1,4 +1,7 @@ package org.gitanimals.guild.domain enum class GuildFarmType { + + DUMMY, + ; } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index e2d0474..7359ca1 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -2,4 +2,7 @@ package org.gitanimals.guild.domain import org.springframework.data.jpa.repository.JpaRepository -interface GuildRepository: JpaRepository +interface GuildRepository : JpaRepository { + + fun existsByTitle(title: String): Boolean +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt new file mode 100644 index 0000000..42f2477 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -0,0 +1,43 @@ +package org.gitanimals.guild.domain + +import org.gitanimals.guild.domain.request.CreateLeaderRequest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class GuildService( + private val guildRepository: GuildRepository, +) { + + fun getGuildById(id: Long): Guild = guildRepository.findByIdOrNull(id) + ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") + + @Transactional + fun createGuild( + guildIcon: String, + title: String, + body: String, + farmType: GuildFarmType, + autoJoin: Boolean, + createLeaderRequest: CreateLeaderRequest, + ) { + require(guildRepository.existsByTitle(title).not()) { + "Cannot create guild cause duplicated guild already exists." + } + + val leader = createLeaderRequest.toDomain() + + val newGuild = Guild.create( + guildIcon = guildIcon, + title = title, + body = body, + farmType = farmType, + leader = leader, + autoJoin = autoJoin, + ) + + guildRepository.save(newGuild) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt b/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt new file mode 100644 index 0000000..951944e --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt @@ -0,0 +1,20 @@ +package org.gitanimals.guild.domain.request + +import org.gitanimals.guild.domain.Leader + +data class CreateLeaderRequest( + val userId: Long, + val name: String, + val personaId: Long, + val contributions: Long, +) { + + fun toDomain(): Leader { + return Leader( + userId = userId, + name = name, + personaId = personaId, + contributions = contributions, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/infra/HttpClientConfigurer.kt b/src/main/kotlin/org/gitanimals/guild/infra/HttpClientConfigurer.kt new file mode 100644 index 0000000..4e5daeb --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/infra/HttpClientConfigurer.kt @@ -0,0 +1,35 @@ +package org.gitanimals.guild.infra + +import org.gitanimals.guild.app.IdentityApi +import org.gitanimals.guild.app.RenderApi +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestClient +import org.springframework.web.client.support.RestClientAdapter +import org.springframework.web.service.invoker.HttpServiceProxyFactory + +@Configuration +class HttpClientConfigurer { + + @Bean + fun identityApiHttpClient(): IdentityApi { + val restClient = RestClient.create("https://api.gitanimals.org") + + val httpServiceProxyFactory = HttpServiceProxyFactory + .builderFor(RestClientAdapter.create(restClient)) + .build() + + return httpServiceProxyFactory.createClient(IdentityApi::class.java) + } + + @Bean + fun renderApiHttpClient(): RenderApi { + val restClient = RestClient.create("https://render.gitanimals.org") + + val httpServiceProxyFactory = HttpServiceProxyFactory + .builderFor(RestClientAdapter.create(restClient)) + .build() + + return httpServiceProxyFactory.createClient(RenderApi::class.java) + } +} diff --git a/src/main/kotlin/org/gitanimals/render/controller/response/UserResponse.kt b/src/main/kotlin/org/gitanimals/render/controller/response/UserResponse.kt index fb61032..7af2d0c 100644 --- a/src/main/kotlin/org/gitanimals/render/controller/response/UserResponse.kt +++ b/src/main/kotlin/org/gitanimals/render/controller/response/UserResponse.kt @@ -5,6 +5,7 @@ import org.gitanimals.render.domain.User data class UserResponse( val id: String, val name: String, + val totalContributions: String, private val personas: List, ) { @@ -13,6 +14,7 @@ data class UserResponse( return UserResponse( user.id.toString(), user.name, + user.contributionCount().toString(), user.personas.map { PersonaResponse( it.id.toString(), diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt new file mode 100644 index 0000000..c10eef3 --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -0,0 +1,123 @@ +package org.gitanimals.guild.app + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import org.gitanimals.guild.app.request.CreateGuildRequest +import org.gitanimals.guild.domain.GuildFarmType +import org.gitanimals.guild.domain.GuildRepository +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.supports.RedisContainer +import org.gitanimals.guild.supports.SagaCapture +import org.rooftop.netx.meta.EnableSaga +import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Application +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import kotlin.time.Duration.Companion.seconds + +@EnableSaga +@DataJpaTest +@ContextConfiguration( + classes = [ + Application::class, + RedisContainer::class, + SagaCapture::class, + CreateGuildFacade::class, + MockApiConfiguration::class, + GuildService::class, + ] +) +@DisplayName("CreateGuildFacade 클래스의") +@EntityScan(basePackages = ["org.gitanimals.guild"]) +@TestPropertySource("classpath:application.properties") +@EnableJpaRepositories(basePackages = ["org.gitanimals.guild"]) +internal class CreateGuildFacadeTest( + private val createGuildFacade: CreateGuildFacade, + private val sagaCapture: SagaCapture, + private val identityApi: IdentityApi, + private val guildRepository: GuildRepository, +) : DescribeSpec({ + + beforeEach { + sagaCapture.clear() + guildRepository.deleteAll() + } + + describe("createGuild 메소드는") { + context("token에 해당하는 유저가 길드를 생성할 수 있는 돈을 갖고 있다면,") { + it("길드를 생성한다.") { + shouldNotThrowAny { + createGuildFacade.createGuild(TOKEN, createGuildRequest) + } + + sagaCapture.countShouldBe( + start = 1, + commit = 1, + ) + } + } + + context("token에 해당하는 유저가 길드를 생성할 수 있는 돈을 갖고 있지 않다면,") { + val poolUserToken = "Bearer pool" + + it("IllegalArgumentException을 던진다,") { + every { identityApi.getUserByToken(poolUserToken) } returns poolIdentityUserResponse + + shouldThrowExactly { + createGuildFacade.createGuild(poolUserToken, createGuildRequest) + } + + eventually(5.seconds) { + sagaCapture.countShouldBe(start = 1, rollback = 1) + } + } + } + + context("포인트 차감 이후 에러가 발생하면,") { + it("유저에게 돈을 돌려준다.") { + // Create Duplicate data set for throw error + createGuildFacade.createGuild(TOKEN, createGuildRequest) + sagaCapture.clear() + + shouldThrowAny { + createGuildFacade.createGuild(TOKEN, createGuildRequest) + } + + eventually(5.seconds) { + sagaCapture.countShouldBe( + start = 1, + commit = 1, + rollback = 1, + ) + } + } + } + } +}) { + private companion object { + private const val TOKEN = "Bearer ..." + + private val createGuildRequest = CreateGuildRequest( + title = "Gitanimals", + body = "We are gitanimals", + guildIcon = "gitanimals.org", + autoJoin = true, + farmType = GuildFarmType.DUMMY + ) + + private val poolIdentityUserResponse = IdentityApi.UserResponse( + id = "1", + username = "devxb", + points = "29999", + profileImage = "https://gitanimals.org" + ) + + } +} diff --git a/src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt b/src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt new file mode 100644 index 0000000..0dee383 --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt @@ -0,0 +1,51 @@ +package org.gitanimals.guild.app + +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean + +@TestConfiguration +class MockApiConfiguration { + + @Bean + fun identityApi(): IdentityApi = mockk().apply { + val api = this + every { api.increasePoint(any(), any(), any(), any()) } just Runs + every { api.decreasePoint(any(), any(), any(), any()) } just Runs + every { api.getUserByToken(any()) } returns identityUserResponse + } + + @Bean + fun renderApi(): RenderApi = mockk().apply { + val api = this + every { api.getUserByName(any()) } returns renderUserResponse + } + + companion object { + val identityUserResponse = IdentityApi.UserResponse( + id = "1", + username = "devxb", + points = "30000", + profileImage = "https://gitanimals.org" + ) + + val renderUserResponse = RenderApi.UserResponse( + id = "2", + name = "devxb", + totalContributions = "9999", + personas = listOf( + RenderApi.UserResponse.PersonaResponse( + id = "3", + level = "99" + ), + RenderApi.UserResponse.PersonaResponse( + id = "4", + level = "98" + ), + ) + ) + } +} diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt new file mode 100644 index 0000000..f2b1f72 --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -0,0 +1,77 @@ +package org.gitanimals.guild.domain + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import org.gitanimals.guild.domain.request.CreateLeaderRequest +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration + +@DataJpaTest +@DisplayName("GuildService 클래스의") +@ContextConfiguration(classes = [GuildService::class]) +@EntityScan(basePackages = ["org.gitanimals.guild"]) +@EnableJpaRepositories(basePackages = ["org.gitanimals.guild"]) +internal class GuildServiceTest( + private val guildService: GuildService, + private val guildRepository: GuildRepository, +) : DescribeSpec({ + + beforeEach { + guildRepository.deleteAll() + } + + describe("createGuild 메소드는") { + context("guild 정보와 leader 정보를 입력받으면") { + val guildIcon = "guildIcon" + val title = "guildTitle" + val body = "guildBody" + val farmType = GuildFarmType.DUMMY + val leaderRequest = CreateLeaderRequest( + userId = 1L, + name = "devxb", + personaId = 2L, + contributions = 3L, + ) + + it("중복된 길드가 아니라면 길드를 생성한다.") { + + shouldNotThrowAny { + guildService.createGuild( + guildIcon = guildIcon, + title = title, + body = body, + farmType = farmType, + createLeaderRequest = leaderRequest, + autoJoin = true, + ) + } + } + + it("중복된 길드라면 IllegalArgumentException을 던진다.") { + guildService.createGuild( + guildIcon = guildIcon, + title = title, + body = body, + farmType = farmType, + createLeaderRequest = leaderRequest, + autoJoin = true, + ) + + shouldThrowExactly { + guildService.createGuild( + guildIcon = guildIcon, + title = title, + body = body, + farmType = farmType, + createLeaderRequest = leaderRequest, + autoJoin = true, + ) + } + } + } + } +}) diff --git a/src/test/kotlin/org/gitanimals/guild/supports/RedisContainer.kt b/src/test/kotlin/org/gitanimals/guild/supports/RedisContainer.kt new file mode 100644 index 0000000..9c7686a --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/supports/RedisContainer.kt @@ -0,0 +1,27 @@ +package org.gitanimals.guild.supports + + +import org.springframework.boot.test.context.TestConfiguration +import org.testcontainers.containers.GenericContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration +internal class RedisContainer { + init { + val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:7.2.3")) + .withExposedPorts(6379) + + runCatching { + redis.start() + }.onFailure { + if (it is com.github.dockerjava.api.exception.InternalServerErrorException) { + redis.start() + } + } + + System.setProperty( + "netx.port", + redis.getMappedPort(6379).toString() + ) + } +} diff --git a/src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt b/src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt new file mode 100644 index 0000000..e83643d --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt @@ -0,0 +1,63 @@ +package org.gitanimals.guild.supports + +import io.kotest.matchers.equals.shouldBeEqual +import org.rooftop.netx.api.* +import org.rooftop.netx.meta.SagaHandler + +@SagaHandler +class SagaCapture { + + val storage = mutableMapOf() + + fun clear() { + storage.clear() + } + + fun countShouldBe( + start: Int = 0, + join: Int = 0, + commit: Int = 0, + rollback: Int = 0, + ) { + startCountShouldBe(start) + joinCountShouldBe(join) + commitCountShouldBe(commit) + rollbackCountShouldBe(rollback) + } + + fun startCountShouldBe(count: Int) { + (storage["start"] ?: 0) shouldBeEqual count + } + + fun joinCountShouldBe(count: Int) { + (storage["join"] ?: 0) shouldBeEqual count + } + + fun commitCountShouldBe(count: Int) { + (storage["commit"] ?: 0) shouldBeEqual count + } + + fun rollbackCountShouldBe(count: Int) { + (storage["rollback"] ?: 0) shouldBeEqual count + } + + @SagaStartListener(successWith = SuccessWith.END) + fun captureStart(startEvent: SagaStartEvent) { + storage["start"] = (storage["start"] ?: 0) + 1 + } + + @SagaJoinListener(successWith = SuccessWith.END) + fun captureJoin(joinEvent: SagaJoinEvent) { + storage["join"] = (storage["join"] ?: 0) + 1 + } + + @SagaCommitListener + fun captureCommit(commitEvent: SagaCommitEvent) { + storage["commit"] = (storage["commit"] ?: 0) + 1 + } + + @SagaRollbackListener + fun captureRollback(rollbackEvent: SagaRollbackEvent) { + storage["rollback"] = (storage["rollback"] ?: 0) + 1 + } +} diff --git a/src/test/kotlin/org/gitanimals/render/domain/UserFixture.kt b/src/test/kotlin/org/gitanimals/render/domain/UserFixture.kt index b177326..e3e719b 100644 --- a/src/test/kotlin/org/gitanimals/render/domain/UserFixture.kt +++ b/src/test/kotlin/org/gitanimals/render/domain/UserFixture.kt @@ -9,7 +9,6 @@ fun user( personas: MutableList = mutableListOf(), contributions: MutableList = mutableListOf(), visit: Long = 0L, - field: FieldType = FieldType.WHITE_FIELD, ): User { return User( id = id, @@ -17,7 +16,6 @@ fun user( personas = personas, contributions = contributions, visit = visit, - field = field, version = 0L, lastPersonaGivePoint = 0, ) From 7177932affe802f8ebbda19b36bb635c6b86fb27 Mon Sep 17 00:00:00 2001 From: devxb Date: Thu, 19 Dec 2024 00:33:06 +0900 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/JoinGuildFacade.kt | 90 +++++++++++++++++++ .../guild/app/event/InboxInputEvent.kt | 88 ++++++++++++++++++ .../guild/controller/GuildController.kt | 14 ++- .../controller/request/JoinGuildRequest.kt | 5 ++ .../org/gitanimals/guild/domain/Guild.kt | 53 ++++++++++- .../gitanimals/guild/domain/GuildService.kt | 40 ++++++++- .../org/gitanimals/guild/domain/Member.kt | 8 +- .../org/gitanimals/guild/domain/WaitMember.kt | 67 ++++++++++++++ .../org/gitanimals/guild/domain/Fixture.kt | 39 ++++++++ .../guild/domain/GuildServiceTest.kt | 70 ++++++++++++++- 10 files changed, 463 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/app/event/InboxInputEvent.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/request/JoinGuildRequest.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt new file mode 100644 index 0000000..4fecbc5 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt @@ -0,0 +1,90 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.app.event.InboxInputEvent +import org.gitanimals.guild.domain.Guild +import org.gitanimals.guild.domain.GuildService +import org.rooftop.netx.api.SagaManager +import org.springframework.stereotype.Service + +@Service +class JoinGuildFacade( + private val renderApi: RenderApi, + private val identityApi: IdentityApi, + private val guildService: GuildService, + private val sagaManager: SagaManager, +) { + + fun joinGuild( + token: String, + guildId: Long, + memberPersonaId: Long, + ) { + val member = identityApi.getUserByToken(token) + val renderInfo = renderApi.getUserByName(member.username) + + require(memberPersonaId in renderInfo.personas.map { it.id.toLong() }) { + "Cannot join guild cause user does not have request member persona id. personaId: \"$memberPersonaId\"" + } + + guildService.joinGuild( + guildId = guildId, + memberUserId = member.id.toLong(), + memberName = member.username, + memberPersonaId = memberPersonaId, + memberContributions = renderInfo.totalContributions.toLong(), + ) + + val guild = guildService.getGuildById(guildId) + if (guild.isAutoJoin()) { + publishNewUserJoinEvents(guild, member) + return + } + + publicGuildJoinRequest(guild, member) + publishSentJoinRequest(guild, member) + } + + private fun publishNewUserJoinEvents( + guild: Guild, + member: IdentityApi.UserResponse, + ) { + guild.getMembers() + .filter { it.userId != member.id.toLong() } + .forEach { + sagaManager.startSync( + InboxInputEvent.newUserJoined( + userId = it.userId, + newUserImage = member.profileImage, + newUserName = member.username, + guildTitle = guild.title, + ) + ) + } + } + + private fun publicGuildJoinRequest( + guild: Guild, + member: IdentityApi.UserResponse + ) { + sagaManager.startSync( + InboxInputEvent.guildJoinRequest( + userId = guild.getLeaderId(), + newUserImage = member.profileImage, + newUserName = member.username, + guildTitle = guild.title, + ) + ) + } + + private fun publishSentJoinRequest( + guild: Guild, + member: IdentityApi.UserResponse, + ) { + sagaManager.startSync( + InboxInputEvent.sentJoinRequest( + userId = member.id.toLong(), + guildTitle = guild.title, + ) + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/app/event/InboxInputEvent.kt b/src/main/kotlin/org/gitanimals/guild/app/event/InboxInputEvent.kt new file mode 100644 index 0000000..bf69ca0 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/event/InboxInputEvent.kt @@ -0,0 +1,88 @@ +package org.gitanimals.guild.app.event + +import org.gitanimals.guild.core.clock +import java.time.Instant + +data class InboxInputEvent( + val inboxData: InboxData, + val publisher: Publisher, +) { + + data class Publisher( + val publisher: String, + val publishedAt: Instant, + ) + + data class InboxData( + val userId: Long, + val type: String = "INBOX", + val title: String, + val body: String, + val image: String, + val redirectTo: String, + ) + + companion object { + + fun newUserJoined( + userId: Long, + newUserImage: String, + newUserName: String, + guildTitle: String, + ): InboxInputEvent { + return InboxInputEvent( + publisher = Publisher( + publisher = "GUILD", + publishedAt = clock.instant(), + ), + inboxData = InboxData( + userId = userId, + title = "New user join", + body = "$newUserName join $guildTitle guild.", + image = newUserImage, + redirectTo = "", + ) + ) + } + + fun guildJoinRequest( + userId: Long, + newUserImage: String, + newUserName: String, + guildTitle: String, + ): InboxInputEvent { + return InboxInputEvent( + publisher = Publisher( + publisher = "GUILD", + publishedAt = clock.instant(), + ), + inboxData = InboxData( + userId = userId, + title = "Guild join request", + body = "$newUserName has sent a join request to the $guildTitle guild.", + image = newUserImage, + redirectTo = "", + ) + ) + } + + fun sentJoinRequest( + userId: Long, + guildTitle: String, + ): InboxInputEvent { + return InboxInputEvent( + publisher = Publisher( + publisher = "GUILD", + publishedAt = clock.instant(), + ), + inboxData = InboxData( + userId = userId, + title = "Guild join request sent", + body = "Guild join request sent to $guildTitle.", + image = "guild-image", // guild 이미지 추가 + redirectTo = "", + ) + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 51233a3..2994ccd 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -1,16 +1,16 @@ package org.gitanimals.guild.controller import org.gitanimals.guild.app.CreateGuildFacade +import org.gitanimals.guild.app.JoinGuildFacade import org.gitanimals.guild.app.request.CreateGuildRequest +import org.gitanimals.guild.controller.request.JoinGuildRequest import org.springframework.http.HttpHeaders -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestHeader -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RestController class GuildController( private val createGuildFacade: CreateGuildFacade, + private val joinGuildFacade: JoinGuildFacade, ) { @PostMapping("/guilds") @@ -20,4 +20,10 @@ class GuildController( ) = createGuildFacade.createGuild(token, createGuildRequest) + @PostMapping("/guilds/{guildId}") + fun joinGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + @RequestBody joinGuildRequest: JoinGuildRequest, + ) = joinGuildFacade.joinGuild(token, guildId, joinGuildRequest.personaId.toLong()) } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/request/JoinGuildRequest.kt b/src/main/kotlin/org/gitanimals/guild/controller/request/JoinGuildRequest.kt new file mode 100644 index 0000000..cfb5bba --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/request/JoinGuildRequest.kt @@ -0,0 +1,5 @@ +package org.gitanimals.guild.controller.request + +data class JoinGuildRequest( + val personaId: String, +) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 1cf2dd5..eef2935 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -33,20 +33,69 @@ class Guild( @Column(name = "farm_type", nullable = false, columnDefinition = "TEXT") val farmType: GuildFarmType, + @Column(name = "auto_join", nullable = false) + private var autoJoin: Boolean, + @OneToMany( mappedBy = "guild", orphanRemoval = true, fetch = FetchType.LAZY, cascade = [CascadeType.ALL], ) - private val members: MutableSet, + private val members: MutableSet = mutableSetOf(), - private var autoJoin: Boolean, + @OneToMany( + mappedBy = "guild", + orphanRemoval = true, + fetch = FetchType.LAZY, + cascade = [CascadeType.ALL], + ) + private val waitMembers: MutableSet = mutableSetOf(), @Version private var version: Long? = null, ) : AbstractTime() { + fun getMembers(): Set = members.toSet() + + fun getWaitMembers(): Set = waitMembers.toSet() + + fun isAutoJoin(): Boolean = autoJoin + + fun join( + memberUserId: Long, + memberName: String, + memberPersonaId: Long, + memberContributions: Long, + ) { + require(leader.userId != memberUserId) { + "Leader cannot join their own guild leaderId: \"${leader.userId}\", memberUserId: \"$memberUserId\"" + } + + if (autoJoin) { + val member = Member.create( + guild = this, + userId = memberUserId, + name = memberName, + personaId = memberPersonaId, + contributions = memberContributions, + ) + members.add(member) + return + } + + val waitMember = WaitMember.create( + guild = this, + userId = memberUserId, + name = memberName, + personaId = memberPersonaId, + contributions = memberContributions, + ) + waitMembers.add(waitMember) + } + + fun getLeaderId(): Long = leader.userId + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 42f2477..b257c02 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -1,6 +1,7 @@ package org.gitanimals.guild.domain import org.gitanimals.guild.domain.request.CreateLeaderRequest +import org.hibernate.Hibernate import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -11,9 +12,6 @@ class GuildService( private val guildRepository: GuildRepository, ) { - fun getGuildById(id: Long): Guild = guildRepository.findByIdOrNull(id) - ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") - @Transactional fun createGuild( guildIcon: String, @@ -40,4 +38,40 @@ class GuildService( guildRepository.save(newGuild) } + + @Transactional + fun joinGuild( + guildId: Long, + memberUserId: Long, + memberName: String, + memberPersonaId: Long, + memberContributions: Long, + ) { + val guild = getGuildById(guildId) + + guild.join( + memberUserId = memberUserId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = memberContributions, + ) + } + + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { + val guild = guildRepository.findByIdOrNull(id) + ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") + + lazyLoading.forEach { it.invoke(guild) } + return guild + } + + companion object { + val loadMembers: (Guild) -> Unit = { + Hibernate.initialize(it.getMembers()) + } + + val loadWaitMembers: (Guild) -> Unit = { + Hibernate.initialize(it.getWaitMembers()) + } + } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index d497371..846a125 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -40,7 +40,13 @@ class Member( companion object { - fun create(guild: Guild, userId: Long, name: String, personaId: Long, contributions: Long): Member { + fun create( + guild: Guild, + userId: Long, + name: String, + personaId: Long, + contributions: Long, + ): Member { return Member( id = IdGenerator.generate(), userId = userId, diff --git a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt new file mode 100644 index 0000000..ec5e373 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt @@ -0,0 +1,67 @@ +package org.gitanimals.guild.domain + +import jakarta.persistence.* +import org.gitanimals.guild.core.IdGenerator + +@Entity +@Table( + name = "wait_member", + indexes = [ + Index( + name = "wait_member_idx_id_user_id", + columnList = "id, user_id", + unique = true, + ) + ] +) +class WaitMember( + @Id + val id: Long, + + @Column(name = "user_id", nullable = false) + val userId: Long, + + @Column(name = "user_name", nullable = false) + val name: String, + + @Column(name = "persona_id", nullable = false) + val personaId: Long, + + @Column(name = "contributions", nullable = false) + private var contributions: Long, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "guild_id") + val guild: Guild, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WaitMember) return false + + return userId == other.userId + } + + override fun hashCode(): Int { + return userId.hashCode() + } + + companion object { + fun create( + guild: Guild, + userId: Long, + name: String, + personaId: Long, + contributions: Long, + ): WaitMember { + return WaitMember( + id = IdGenerator.generate(), + userId = userId, + name = name, + personaId = personaId, + guild = guild, + contributions = contributions, + ) + } + } +} diff --git a/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt new file mode 100644 index 0000000..c1599ff --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt @@ -0,0 +1,39 @@ +package org.gitanimals.guild.domain + +fun guild( + id: Long = 1L, + guildIcon: String = "default_icon.png", + title: String = "Default Guild Title", + body: String = "Default guild description.", + leader: Leader = leader(), + members: MutableSet = mutableSetOf(), + waitMembers: MutableSet = mutableSetOf(), + farmType: GuildFarmType = GuildFarmType.DUMMY, + autoJoin: Boolean = true, +): Guild { + return Guild( + id = id, + guildIcon = guildIcon, + title = title, + body = body, + leader = leader, + members = members, + waitMembers = waitMembers, + farmType = farmType, + autoJoin = autoJoin, + ) +} + +fun leader( + userId: Long = 1L, + name: String = "Default Leader", + personaId: Long = 1L, + contributions: Long = 0L, +): Leader { + return Leader( + userId = userId, + name = name, + personaId = personaId, + contributions = contributions + ) +} diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt index f2b1f72..641d5e2 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -4,6 +4,9 @@ import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import org.gitanimals.guild.domain.GuildService.Companion.loadMembers +import org.gitanimals.guild.domain.GuildService.Companion.loadWaitMembers import org.gitanimals.guild.domain.request.CreateLeaderRequest import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -20,7 +23,7 @@ internal class GuildServiceTest( private val guildRepository: GuildRepository, ) : DescribeSpec({ - beforeEach { + afterEach { guildRepository.deleteAll() } @@ -74,4 +77,69 @@ internal class GuildServiceTest( } } } + + describe("joinGuild 메소드는") { + context("guild가 autoJoin true라면,") { + val guild = guildRepository.save(guild()) + val memberUserId = 2L + val memberName = "devxb" + val memberPersonaId = 2L + val memberContributions = 3L + + + it("유저를 바로 길드에 가입시킨다.") { + guildService.joinGuild( + guildId = guild.id, + memberUserId = memberUserId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = memberContributions, + ) + + guildService.getGuildById(guild.id, loadMembers).getMembers().size shouldBe 1 + } + } + + context("guild가 autoJoin false라면,") { + val guild = guildRepository.save(guild(autoJoin = false)) + val memberUserId = 2L + val memberName = "devxb" + val memberPersonaId = 2L + val memberContributions = 3L + + + it("유저를 wait 대기열에 포함시킨다.") { + guildService.joinGuild( + guildId = guild.id, + memberUserId = memberUserId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = memberContributions, + ) + + guildService.getGuildById(guild.id, loadWaitMembers) + .getWaitMembers().size shouldBe 1 + } + } + + context("가입을 요청한 유저와 리더의 아이디가 같다면,") { + val memberUserId = 1L + val guild = guildRepository.save(guild(leader = leader(userId = memberUserId))) + val memberName = "devxb" + val memberPersonaId = 2L + val memberContributions = 3L + + it("IllegalArgumentException을 던진다.") { + shouldThrowExactly { + guildService.joinGuild( + guildId = guild.id, + memberUserId = memberUserId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = memberContributions, + ) + } + } + } + } }) From 8115da75b5633e0999af2b35aa75950497e2050f Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 20 Dec 2024 20:39:00 +0900 Subject: [PATCH 07/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=88=98=EB=9D=BD=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=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 --- .../guild/app/AcceptJoinGuildFacade.kt | 17 ++++++++++++ .../guild/controller/GuildController.kt | 9 +++++++ .../org/gitanimals/guild/domain/Guild.kt | 7 +++++ .../guild/domain/GuildRepository.kt | 11 ++++++++ .../gitanimals/guild/domain/GuildService.kt | 8 ++++++ .../org/gitanimals/guild/domain/WaitMember.kt | 8 ++++++ .../guild/domain/GuildServiceTest.kt | 27 +++++++++++++++++++ 7 files changed, 87 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt new file mode 100644 index 0000000..9d4af32 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt @@ -0,0 +1,17 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Service + +@Service +class AcceptJoinGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun acceptJoin(token: String, guildId: Long, acceptUserId: Long) { + val user = identityApi.getUserByToken(token) + + guildService.acceptJoin(user.id.toLong(), guildId = guildId, acceptUserId = acceptUserId) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 2994ccd..50b48e3 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -1,5 +1,6 @@ package org.gitanimals.guild.controller +import org.gitanimals.guild.app.AcceptJoinGuildFacade import org.gitanimals.guild.app.CreateGuildFacade import org.gitanimals.guild.app.JoinGuildFacade import org.gitanimals.guild.app.request.CreateGuildRequest @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.* class GuildController( private val createGuildFacade: CreateGuildFacade, private val joinGuildFacade: JoinGuildFacade, + private val acceptJoinGuildFacade: AcceptJoinGuildFacade, ) { @PostMapping("/guilds") @@ -26,4 +28,11 @@ class GuildController( @PathVariable("guildId") guildId: Long, @RequestBody joinGuildRequest: JoinGuildRequest, ) = joinGuildFacade.joinGuild(token, guildId, joinGuildRequest.personaId.toLong()) + + @PostMapping("/guilds/{guildId}/accepts") + fun acceptJoinGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + @RequestParam("user-id") acceptUserId: Long, + ) = acceptJoinGuildFacade.acceptJoin(token, guildId, acceptUserId) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index eef2935..34be60c 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -96,6 +96,13 @@ class Guild( fun getLeaderId(): Long = leader.userId + fun accept(acceptUserId: Long) { + val acceptUser = waitMembers.firstOrNull { it.userId == acceptUserId } ?: return + waitMembers.remove(acceptUser) + + members.add(acceptUser.toMember()) + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index 7359ca1..1b8dad9 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -1,8 +1,19 @@ package org.gitanimals.guild.domain import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param interface GuildRepository : JpaRepository { fun existsByTitle(title: String): Boolean + + @Query( + """ + select g from Guild as g + where g.id = :id + and g.leader.userId = :leaderId + """ + ) + fun findGuildByIdAndUserId(@Param("id") id: Long, @Param("leaderId") leaderId: Long): Guild? } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index b257c02..c74d4b3 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -57,6 +57,14 @@ class GuildService( ) } + @Transactional + fun acceptJoin(acceptorId: Long, guildId: Long, acceptUserId: Long) { + val guild = guildRepository.findGuildByIdAndUserId(guildId, acceptorId) + ?: throw IllegalArgumentException("Cannot accept join cause your not leader.") + + guild.accept(acceptUserId) + } + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { val guild = guildRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") diff --git a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt index ec5e373..bd6bf36 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt @@ -35,6 +35,14 @@ class WaitMember( val guild: Guild, ) { + fun toMember(): Member = Member.create( + userId = userId, + name = name, + personaId = personaId, + guild = guild, + contributions = contributions, + ) + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is WaitMember) return false diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt index 641d5e2..6eb5f87 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -142,4 +142,31 @@ internal class GuildServiceTest( } } } + + describe("acceptJoinGuild 메소드는") { + context("가입을 수락한 사람이 길드 리더라면,") { + val guild = guildRepository.save(guild(autoJoin = false)) + val memberUserId = 2L + val memberName = "devxb" + val memberPersonaId = 2L + val memberContributions = 3L + + guildService.joinGuild( + guildId = guild.id, + memberUserId = memberUserId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = memberContributions, + ) + + it("멤버를 가입시킨다.") { + guildService.acceptJoin(acceptorId = 1L, guildId = guild.id, acceptUserId = memberUserId) + + val result = guildService.getGuildById(guild.id, loadWaitMembers, loadMembers) + + result.getWaitMembers().size shouldBe 0 + result.getMembers().size shouldBe 1 + } + } + } }) From 42398e5a569d6cb841559acb31fc40d06d91da11 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 20 Dec 2024 20:58:53 +0900 Subject: [PATCH 08/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EB=B0=A9=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/KickGuildFacade.kt | 21 ++++++++++++++ .../guild/controller/GuildController.kt | 9 ++++++ .../org/gitanimals/guild/domain/Guild.kt | 4 +++ .../guild/domain/GuildRepository.kt | 2 +- .../gitanimals/guild/domain/GuildService.kt | 12 ++++++-- .../org/gitanimals/guild/domain/Fixture.kt | 16 ++++++++++ .../guild/domain/GuildServiceTest.kt | 29 ++++++++++++++++++- 7 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/KickGuildFacade.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/KickGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/KickGuildFacade.kt new file mode 100644 index 0000000..66add63 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/KickGuildFacade.kt @@ -0,0 +1,21 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Service + +@Service +class KickGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun kickMember(token: String, guildId: Long, kickUserId: Long) { + val user = identityApi.getUserByToken(token) + + guildService.kickMember( + kickerId = user.id.toLong(), + guildId = guildId, + kickUserId = kickUserId, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 50b48e3..7e546e5 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -3,6 +3,7 @@ package org.gitanimals.guild.controller import org.gitanimals.guild.app.AcceptJoinGuildFacade import org.gitanimals.guild.app.CreateGuildFacade import org.gitanimals.guild.app.JoinGuildFacade +import org.gitanimals.guild.app.KickGuildFacade import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest import org.springframework.http.HttpHeaders @@ -13,6 +14,7 @@ class GuildController( private val createGuildFacade: CreateGuildFacade, private val joinGuildFacade: JoinGuildFacade, private val acceptJoinGuildFacade: AcceptJoinGuildFacade, + private val kickGuildFacade: KickGuildFacade, ) { @PostMapping("/guilds") @@ -35,4 +37,11 @@ class GuildController( @PathVariable("guildId") guildId: Long, @RequestParam("user-id") acceptUserId: Long, ) = acceptJoinGuildFacade.acceptJoin(token, guildId, acceptUserId) + + @DeleteMapping("/guilds/{guildId}") + fun kickFromGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + @RequestParam("user-id") kickUserId: Long, + ) = kickGuildFacade.kickMember(token, guildId, kickUserId) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 34be60c..79d0f83 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -103,6 +103,10 @@ class Guild( members.add(acceptUser.toMember()) } + fun kickMember(kickUserId: Long) { + members.removeIf { it.userId == kickUserId } + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index 1b8dad9..2a32f0e 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -15,5 +15,5 @@ interface GuildRepository : JpaRepository { and g.leader.userId = :leaderId """ ) - fun findGuildByIdAndUserId(@Param("id") id: Long, @Param("leaderId") leaderId: Long): Guild? + fun findGuildByIdAndLeaderId(@Param("id") id: Long, @Param("leaderId") leaderId: Long): Guild? } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index c74d4b3..0490d9f 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -59,12 +59,20 @@ class GuildService( @Transactional fun acceptJoin(acceptorId: Long, guildId: Long, acceptUserId: Long) { - val guild = guildRepository.findGuildByIdAndUserId(guildId, acceptorId) - ?: throw IllegalArgumentException("Cannot accept join cause your not leader.") + val guild = guildRepository.findGuildByIdAndLeaderId(guildId, acceptorId) + ?: throw IllegalArgumentException("Cannot accept join cause your not a leader.") guild.accept(acceptUserId) } + @Transactional + fun kickMember(kickerId: Long, guildId: Long, kickUserId: Long) { + val guild = guildRepository.findGuildByIdAndLeaderId(guildId, kickerId) + ?: throw IllegalArgumentException("Cannot kick member cause your not a leader.") + + guild.kickMember(kickUserId) + } + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { val guild = guildRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") diff --git a/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt index c1599ff..a72ca28 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt @@ -37,3 +37,19 @@ fun leader( contributions = contributions ) } + +fun member( + guild: Guild, + userId: Long = 2L, + name: String = "DefaultName", + personaId: Long = 200L, + contributions: Long = 500L +): Member { + return Member.create( + guild = guild, + userId = userId, + name = name, + personaId = personaId, + contributions = contributions + ) +} diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt index 6eb5f87..5060f17 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -160,7 +160,11 @@ internal class GuildServiceTest( ) it("멤버를 가입시킨다.") { - guildService.acceptJoin(acceptorId = 1L, guildId = guild.id, acceptUserId = memberUserId) + guildService.acceptJoin( + acceptorId = 1L, + guildId = guild.id, + acceptUserId = memberUserId + ) val result = guildService.getGuildById(guild.id, loadWaitMembers, loadMembers) @@ -169,4 +173,27 @@ internal class GuildServiceTest( } } } + + describe("kickMember 메소드는") { + context("추방을 요청한 사람이 길드 리더라면,") { + val memberId = 2L + val guild = guildRepository.save( + guild(autoJoin = false).apply { + member(guild = this, userId = memberId) + } + ) + + it("멤버를 추방시킨다.") { + guildService.kickMember( + kickerId = guild.getLeaderId(), + guildId = guild.id, + kickUserId = memberId + ) + + val result = guildService.getGuildById(guild.id, loadWaitMembers, loadMembers) + + result.getMembers().size shouldBe 0 + } + } + } }) From 0363b8c5a64fc0ddeb147a57eeb9d89bb11fe519 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 20 Dec 2024 21:33:50 +0900 Subject: [PATCH 09/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/ChangeGuildFacade.kt | 22 +++++++++++++++++++ .../guild/controller/GuildController.kt | 14 ++++++++---- .../org/gitanimals/guild/domain/Guild.kt | 19 +++++++++++----- .../gitanimals/guild/domain/GuildService.kt | 9 ++++++++ .../domain/request/ChangeGuildRequest.kt | 11 ++++++++++ 5 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/request/ChangeGuildRequest.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt new file mode 100644 index 0000000..8e9ae83 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/ChangeGuildFacade.kt @@ -0,0 +1,22 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.domain.request.ChangeGuildRequest +import org.springframework.stereotype.Service + +@Service +class ChangeGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun changeGuild(token: String, guildId: Long, changeGuildRequest: ChangeGuildRequest) { + val user = identityApi.getUserByToken(token) + + guildService.changeGuild( + changeRequesterId = user.id.toLong(), + guildId = guildId, + request = changeGuildRequest, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 7e546e5..cb86e0e 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -1,11 +1,9 @@ package org.gitanimals.guild.controller -import org.gitanimals.guild.app.AcceptJoinGuildFacade -import org.gitanimals.guild.app.CreateGuildFacade -import org.gitanimals.guild.app.JoinGuildFacade -import org.gitanimals.guild.app.KickGuildFacade +import org.gitanimals.guild.app.* import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest +import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.springframework.http.HttpHeaders import org.springframework.web.bind.annotation.* @@ -15,6 +13,7 @@ class GuildController( private val joinGuildFacade: JoinGuildFacade, private val acceptJoinGuildFacade: AcceptJoinGuildFacade, private val kickGuildFacade: KickGuildFacade, + private val changeGuildFacade: ChangeGuildFacade, ) { @PostMapping("/guilds") @@ -44,4 +43,11 @@ class GuildController( @PathVariable("guildId") guildId: Long, @RequestParam("user-id") kickUserId: Long, ) = kickGuildFacade.kickMember(token, guildId, kickUserId) + + @PatchMapping("/guilds/{guildId}") + fun changeGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + @RequestBody changeGuildRequest: ChangeGuildRequest, + ) = changeGuildFacade.changeGuild(token, guildId, changeGuildRequest) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 79d0f83..382e659 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -3,6 +3,7 @@ package org.gitanimals.guild.domain import jakarta.persistence.* import org.gitanimals.guild.core.AggregateRoot import org.gitanimals.guild.core.IdGenerator +import org.gitanimals.guild.domain.request.ChangeGuildRequest @Entity @AggregateRoot @@ -18,20 +19,20 @@ class Guild( val id: Long, @Column(name = "guild_icon", nullable = false) - val guildIcon: String, + private var guildIcon: String, @Column(name = "title", unique = true, nullable = false, length = 50) - val title: String, + private var title: String, @Column(name = "body", columnDefinition = "TEXT", length = 500) - val body: String, + private var body: String, @Embedded - val leader: Leader, + private val leader: Leader, @Enumerated(EnumType.STRING) @Column(name = "farm_type", nullable = false, columnDefinition = "TEXT") - val farmType: GuildFarmType, + private var farmType: GuildFarmType, @Column(name = "auto_join", nullable = false) private var autoJoin: Boolean, @@ -107,6 +108,14 @@ class Guild( members.removeIf { it.userId == kickUserId } } + fun change(request: ChangeGuildRequest) { + this.title = request.title + this.body = request.body + this.farmType = request.farmType + this.guildIcon = request.guildIcon + this.autoJoin = request.autoJoin + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 0490d9f..9cc0742 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -1,5 +1,6 @@ package org.gitanimals.guild.domain +import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.gitanimals.guild.domain.request.CreateLeaderRequest import org.hibernate.Hibernate import org.springframework.data.repository.findByIdOrNull @@ -73,6 +74,13 @@ class GuildService( guild.kickMember(kickUserId) } + fun changeGuild(changeRequesterId: Long, guildId: Long, request: ChangeGuildRequest) { + val guild = guildRepository.findGuildByIdAndLeaderId(guildId, changeRequesterId) + ?: throw IllegalArgumentException("Cannot kick member cause your not a leader.") + + guild.change(request) + } + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { val guild = guildRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") @@ -81,6 +89,7 @@ class GuildService( return guild } + companion object { val loadMembers: (Guild) -> Unit = { Hibernate.initialize(it.getMembers()) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/request/ChangeGuildRequest.kt b/src/main/kotlin/org/gitanimals/guild/domain/request/ChangeGuildRequest.kt new file mode 100644 index 0000000..7a92bc3 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/request/ChangeGuildRequest.kt @@ -0,0 +1,11 @@ +package org.gitanimals.guild.domain.request + +import org.gitanimals.guild.domain.GuildFarmType + +data class ChangeGuildRequest( + val title: String, + val body: String, + val farmType: GuildFarmType, + val guildIcon: String, + val autoJoin: Boolean, +) From b6faee6c1f1e5c88a886a3410ada88e95b9e2a53 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 20 Dec 2024 21:54:33 +0900 Subject: [PATCH 10/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20id=EB=A1=9C?= =?UTF-8?q?=20=EA=B8=B8=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=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 --- .../guild/controller/GuildController.kt | 25 +++++- .../controller/response/GuildResponse.kt | 83 +++++++++++++++++++ .../org/gitanimals/guild/domain/Guild.kt | 19 +++++ .../org/gitanimals/guild/domain/Leader.kt | 2 +- .../org/gitanimals/guild/domain/Member.kt | 2 + .../org/gitanimals/guild/domain/WaitMember.kt | 2 + 6 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index cb86e0e..ad8a2a6 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -3,12 +3,16 @@ package org.gitanimals.guild.controller import org.gitanimals.guild.app.* import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest +import org.gitanimals.guild.controller.response.GuildResponse +import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* @RestController class GuildController( + private val guildService: GuildService, private val createGuildFacade: CreateGuildFacade, private val joinGuildFacade: JoinGuildFacade, private val acceptJoinGuildFacade: AcceptJoinGuildFacade, @@ -16,13 +20,14 @@ class GuildController( private val changeGuildFacade: ChangeGuildFacade, ) { + @ResponseStatus(HttpStatus.OK) @PostMapping("/guilds") fun createGuild( @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, @RequestBody createGuildRequest: CreateGuildRequest, ) = createGuildFacade.createGuild(token, createGuildRequest) - + @ResponseStatus(HttpStatus.OK) @PostMapping("/guilds/{guildId}") fun joinGuild( @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, @@ -30,6 +35,8 @@ class GuildController( @RequestBody joinGuildRequest: JoinGuildRequest, ) = joinGuildFacade.joinGuild(token, guildId, joinGuildRequest.personaId.toLong()) + + @ResponseStatus(HttpStatus.OK) @PostMapping("/guilds/{guildId}/accepts") fun acceptJoinGuild( @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, @@ -37,6 +44,8 @@ class GuildController( @RequestParam("user-id") acceptUserId: Long, ) = acceptJoinGuildFacade.acceptJoin(token, guildId, acceptUserId) + + @ResponseStatus(HttpStatus.OK) @DeleteMapping("/guilds/{guildId}") fun kickFromGuild( @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, @@ -44,10 +53,24 @@ class GuildController( @RequestParam("user-id") kickUserId: Long, ) = kickGuildFacade.kickMember(token, guildId, kickUserId) + + @ResponseStatus(HttpStatus.OK) @PatchMapping("/guilds/{guildId}") fun changeGuild( @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, @PathVariable("guildId") guildId: Long, @RequestBody changeGuildRequest: ChangeGuildRequest, ) = changeGuildFacade.changeGuild(token, guildId, changeGuildRequest) + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/guilds/{guildId}") + fun getGuildById(guildId: Long): GuildResponse { + val guild = guildService.getGuildById( + guildId, + GuildService.loadMembers, + GuildService.loadWaitMembers, + ) + + return GuildResponse.from(guild) + } } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt new file mode 100644 index 0000000..aa0e70b --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt @@ -0,0 +1,83 @@ +package org.gitanimals.guild.controller.response + +import com.fasterxml.jackson.annotation.JsonFormat +import org.gitanimals.guild.domain.Guild +import java.time.Instant + +data class GuildResponse( + val id: String, + val title: String, + val body: String, + val guildIcon: String, + val leader: Leader, + val farmType: String, + val totalContributions: String, + val members: List, + val waitMembers: List, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "UTC" + ) + val createdAt: Instant, +) { + data class Leader( + val userId: String, + val name: String, + val contributions: String, + ) + + data class Member( + val id: String, + val userId: String, + val name: String, + val contributions: String, + val personaId: String, + ) + + data class WaitMember( + val id: String, + val userId: String, + val name: String, + val contributions: String, + val personaId: String, + ) + + companion object { + + fun from(guild: Guild): GuildResponse { + return GuildResponse( + id = guild.id.toString(), + title = guild.getTitle(), + body = guild.getBody(), + guildIcon = guild.getGuildIcon(), + leader = Leader( + userId = guild.getLeaderId().toString(), + name = guild.getLeaderName(), + contributions = guild.getContributions().toString(), + ), + farmType = guild.getGuildFarmType().toString(), + totalContributions = guild.getTotalContributions().toString(), + members = guild.getMembers().map { + Member( + id = it.id.toString(), + userId = it.userId.toString(), + name = it.name, + contributions = it.getContributions().toString(), + personaId = it.personaId.toString(), + ) + }, + waitMembers = guild.getWaitMembers().map { + WaitMember( + id = it.id.toString(), + userId = it.userId.toString(), + name = it.name, + contributions = it.getContributions().toString(), + personaId = it.personaId.toString(), + ) + }, + createdAt = guild.createdAt, + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 382e659..d767c95 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -4,6 +4,7 @@ import jakarta.persistence.* import org.gitanimals.guild.core.AggregateRoot import org.gitanimals.guild.core.IdGenerator import org.gitanimals.guild.domain.request.ChangeGuildRequest +import org.hibernate.annotations.BatchSize @Entity @AggregateRoot @@ -43,6 +44,7 @@ class Guild( fetch = FetchType.LAZY, cascade = [CascadeType.ALL], ) + @BatchSize(size = 10) private val members: MutableSet = mutableSetOf(), @OneToMany( @@ -51,6 +53,7 @@ class Guild( fetch = FetchType.LAZY, cascade = [CascadeType.ALL], ) + @BatchSize(size = 10) private val waitMembers: MutableSet = mutableSetOf(), @Version @@ -116,6 +119,22 @@ class Guild( this.autoJoin = request.autoJoin } + fun getTitle(): String = title + + fun getBody(): String = body + + fun getGuildIcon(): String = guildIcon + + fun getLeaderName(): String = leader.name + + fun getContributions(): Long = leader.contributions + + fun getGuildFarmType(): GuildFarmType = farmType + + fun getTotalContributions(): Long { + return leader.contributions + members.sumOf { it.getContributions() } + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt index 65a3710..61f7b8f 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt @@ -15,5 +15,5 @@ data class Leader( val personaId: Long, @Column(name = "contributions", nullable = false) - private var contributions: Long, + var contributions: Long, ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index 846a125..701f90d 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -27,6 +27,8 @@ class Member( val guild: Guild, ) : AbstractTime() { + fun getContributions() = contributions + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Member) return false diff --git a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt index bd6bf36..2c1212c 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt @@ -35,6 +35,8 @@ class WaitMember( val guild: Guild, ) { + fun getContributions(): Long = contributions + fun toMember(): Member = Member.create( userId = userId, name = name, From 5dfd8113205684392d130357b265b22af66185b2 Mon Sep 17 00:00:00 2001 From: devxb Date: Fri, 20 Dec 2024 22:21:09 +0900 Subject: [PATCH 11/24] =?UTF-8?q?feat:=20=EB=82=B4=EA=B0=80=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=ED=95=9C=20=EA=B8=B8=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/app/GetJoinedGuildFacade.kt | 18 ++++++++++++++++++ .../guild/controller/GuildController.kt | 12 ++++++++++++ .../controller/response/GuildsResponse.kt | 15 +++++++++++++++ .../gitanimals/guild/domain/GuildRepository.kt | 9 +++++++++ .../gitanimals/guild/domain/GuildService.kt | 6 ++++++ 5 files changed, 60 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/response/GuildsResponse.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt new file mode 100644 index 0000000..a21e5f2 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt @@ -0,0 +1,18 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.Guild +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Service + +@Service +class GetJoinedGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun getJoinedGuilds(token: String): List { + val user = identityApi.getUserByToken(token) + + return guildService.findAllGuildByUserId(user.id) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index ad8a2a6..6c9607e 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -4,6 +4,7 @@ import org.gitanimals.guild.app.* import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest import org.gitanimals.guild.controller.response.GuildResponse +import org.gitanimals.guild.controller.response.GuildsResponse import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.springframework.http.HttpHeaders @@ -18,6 +19,7 @@ class GuildController( private val acceptJoinGuildFacade: AcceptJoinGuildFacade, private val kickGuildFacade: KickGuildFacade, private val changeGuildFacade: ChangeGuildFacade, + private val joinedGuildFacade: GetJoinedGuildFacade, ) { @ResponseStatus(HttpStatus.OK) @@ -73,4 +75,14 @@ class GuildController( return GuildResponse.from(guild) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/guilds") + fun getAllJoinedGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + ): GuildsResponse { + val guilds = joinedGuildFacade.getJoinedGuilds(token) + + return GuildsResponse.from(guilds) + } } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildsResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildsResponse.kt new file mode 100644 index 0000000..d494404 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildsResponse.kt @@ -0,0 +1,15 @@ +package org.gitanimals.guild.controller.response + +import org.gitanimals.guild.domain.Guild + +data class GuildsResponse( + val guilds: List, +) { + + companion object { + + fun from(guilds: List): GuildsResponse { + return GuildsResponse(guilds.map { GuildResponse.from(it) }) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index 2a32f0e..5976a80 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -16,4 +16,13 @@ interface GuildRepository : JpaRepository { """ ) fun findGuildByIdAndLeaderId(@Param("id") id: Long, @Param("leaderId") leaderId: Long): Guild? + + @Query( + """ + select g from Guild as g + join fetch g.members as m + where g.leader.userId = :userId or m.userId = :userId + """ + ) + fun findAllGuildByUserIdWithMembers(@Param("userId") userId: String): List } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 9cc0742..fd19772 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -89,6 +89,12 @@ class GuildService( return guild } + fun findAllGuildByUserId(userId: String): List { + return guildRepository.findAllGuildByUserIdWithMembers(userId).apply { + this.forEach { loadWaitMembers.invoke(it) } + } + } + companion object { val loadMembers: (Guild) -> Unit = { From 8954b8407f4afda3633c02b6adb731662a3f868b Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 17:37:35 +0900 Subject: [PATCH 12/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/SearchGuildFacade.kt | 32 ++++++++ .../guild/controller/GuildController.kt | 21 ++++++ .../response/GuildPagingResponse.kt | 40 ++++++++++ .../guild/domain/GuildRepository.kt | 16 ++++ .../gitanimals/guild/domain/GuildService.kt | 22 ++++++ .../guild/domain/RandomGuildCache.kt | 8 ++ .../gitanimals/guild/domain/SearchFilter.kt | 36 +++++++++ .../guild/infra/InMemoryRandomGuildCache.kt | 75 +++++++++++++++++++ 8 files changed, 250 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/SearchGuildFacade.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/response/GuildPagingResponse.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/RandomGuildCache.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/SearchFilter.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/infra/InMemoryRandomGuildCache.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/SearchGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/SearchGuildFacade.kt new file mode 100644 index 0000000..507a80b --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/SearchGuildFacade.kt @@ -0,0 +1,32 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.Guild +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.domain.RandomGuildCache +import org.gitanimals.guild.domain.SearchFilter +import org.springframework.data.domain.Page +import org.springframework.stereotype.Service + +@Service +class SearchGuildFacade( + private val randomGuildCache: RandomGuildCache, + private val guildService: GuildService, +) { + + fun search(key: Int, text: String, pageNumber: Int, filter: SearchFilter): Page { + if (filter == SearchFilter.RANDOM) { + return randomGuildCache.get( + key = key, + text = text, + pageNumber = pageNumber, + filter = filter, + ) + } + + return guildService.search( + text = text, + pageNumber = pageNumber, + filter = filter, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 6c9607e..71a2105 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -3,9 +3,11 @@ package org.gitanimals.guild.controller import org.gitanimals.guild.app.* import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest +import org.gitanimals.guild.controller.response.GuildPagingResponse import org.gitanimals.guild.controller.response.GuildResponse import org.gitanimals.guild.controller.response.GuildsResponse import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.domain.SearchFilter import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus @@ -20,6 +22,7 @@ class GuildController( private val kickGuildFacade: KickGuildFacade, private val changeGuildFacade: ChangeGuildFacade, private val joinedGuildFacade: GetJoinedGuildFacade, + private val searchGuildFacade: SearchGuildFacade, ) { @ResponseStatus(HttpStatus.OK) @@ -85,4 +88,22 @@ class GuildController( return GuildsResponse.from(guilds) } + + @ResponseStatus(HttpStatus.OK) + @GetMapping("/guilds/search") + fun searchGuilds( + @RequestParam(name = "text", defaultValue = "") text: String, + @RequestParam(name = "page-number", defaultValue = "0") pageNumber: Int, + @RequestParam(name = "filter", defaultValue = "RANDOM") filter: SearchFilter, + @RequestParam(name = "key", defaultValue = "0") key: Int, + ): GuildPagingResponse { + val guilds = searchGuildFacade.search( + key = key, + text = text, + pageNumber = pageNumber, + filter = filter, + ) + + return GuildPagingResponse.from(guilds) + } } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildPagingResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildPagingResponse.kt new file mode 100644 index 0000000..3fd285c --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildPagingResponse.kt @@ -0,0 +1,40 @@ +package org.gitanimals.guild.controller.response + +import org.gitanimals.guild.domain.Guild +import org.springframework.data.domain.Page + +data class GuildPagingResponse( + val guilds: List, + val pagination: Pagination, +) { + + data class Pagination( + val totalRecords: Int, + val currentPage: Int, + val totalPages: Int, + val nextPage: Int?, + val prevPage: Int?, + ) + + companion object { + + fun from(guilds: Page): GuildPagingResponse { + return GuildPagingResponse( + guilds = guilds.map { GuildResponse.from(it) }.toList(), + pagination = Pagination( + totalRecords = guilds.count(), + currentPage = guilds.number, + totalPages = guilds.totalPages, + nextPage = when (guilds.hasNext()) { + true -> guilds.number + 1 + false -> null + }, + prevPage = when (guilds.hasPrevious()) { + true -> guilds.number - 1 + false -> null + }, + ) + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index 5976a80..e65a359 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -1,5 +1,7 @@ package org.gitanimals.guild.domain +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param @@ -25,4 +27,18 @@ interface GuildRepository : JpaRepository { """ ) fun findAllGuildByUserIdWithMembers(@Param("userId") userId: String): List + + @Query("select g from Guild as g") + fun findAllWithLimit(pageable: Pageable): List + + @Query( + value = """ + SELECT g.* + FROM guild g + WHERE MATCH(g.title, g.body) AGAINST(:text IN BOOLEAN MODE) + """, + countQuery = "SELECT COUNT(*) FROM guild", + nativeQuery = true, + ) + fun search(@Param("text") text: String, pageable: Pageable): Page } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index fd19772..762c38a 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -3,6 +3,8 @@ package org.gitanimals.guild.domain import org.gitanimals.guild.domain.request.ChangeGuildRequest import org.gitanimals.guild.domain.request.CreateLeaderRequest import org.hibernate.Hibernate +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -74,6 +76,7 @@ class GuildService( guild.kickMember(kickUserId) } + @Transactional fun changeGuild(changeRequesterId: Long, guildId: Long, request: ChangeGuildRequest) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, changeRequesterId) ?: throw IllegalArgumentException("Cannot kick member cause your not a leader.") @@ -95,8 +98,27 @@ class GuildService( } } + fun search(text: String, pageNumber: Int, filter: SearchFilter): Page { + return guildRepository.search(text, Pageable.ofSize(PAGE_SIZE).withPage(pageNumber)).apply { + this.forEach { + loadMembers.invoke(it) + loadWaitMembers.invoke(it) + } + } + } + + fun findAllWithLimit(limit: Int): List { + return guildRepository.findAllWithLimit(Pageable.ofSize(limit)).apply { + this.forEach { + loadMembers.invoke(it) + loadWaitMembers.invoke(it) + } + } + } companion object { + const val PAGE_SIZE = 9 + val loadMembers: (Guild) -> Unit = { Hibernate.initialize(it.getMembers()) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/RandomGuildCache.kt b/src/main/kotlin/org/gitanimals/guild/domain/RandomGuildCache.kt new file mode 100644 index 0000000..d8661aa --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/RandomGuildCache.kt @@ -0,0 +1,8 @@ +package org.gitanimals.guild.domain + +import org.springframework.data.domain.Page + +fun interface RandomGuildCache { + + fun get(key: Int, text: String, pageNumber: Int, filter: SearchFilter): Page +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/SearchFilter.kt b/src/main/kotlin/org/gitanimals/guild/domain/SearchFilter.kt new file mode 100644 index 0000000..6a8f4c5 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/SearchFilter.kt @@ -0,0 +1,36 @@ +package org.gitanimals.guild.domain + +enum class SearchFilter { + + RANDOM { + override fun sort(guilds: List) = guilds + }, + + PEOPLE_ASC { + override fun sort(guilds: List): List { + return guilds.sortedBy { it.getMembers().size } + } + }, + + PEOPLE_DESC { + override fun sort(guilds: List): List { + return guilds.sortedByDescending { it.getMembers().size } + + } + }, + + CONTRIBUTION_ASC { + override fun sort(guilds: List): List { + return guilds.sortedBy { it.getTotalContributions() } + } + }, + + CONTRIBUTION_DESC { + override fun sort(guilds: List): List { + return guilds.sortedByDescending { it.getTotalContributions() } + } + }, + ; + + abstract fun sort(guilds: List): List +} diff --git a/src/main/kotlin/org/gitanimals/guild/infra/InMemoryRandomGuildCache.kt b/src/main/kotlin/org/gitanimals/guild/infra/InMemoryRandomGuildCache.kt new file mode 100644 index 0000000..1c0ebba --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/infra/InMemoryRandomGuildCache.kt @@ -0,0 +1,75 @@ +package org.gitanimals.guild.infra + +import org.gitanimals.guild.domain.Guild +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.domain.GuildService.Companion.PAGE_SIZE +import org.gitanimals.guild.domain.RandomGuildCache +import org.gitanimals.guild.domain.SearchFilter +import org.springframework.context.event.ContextRefreshedEvent +import org.springframework.context.event.EventListener +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class InMemoryRandomGuildCache( + private val guildService: GuildService, +) : RandomGuildCache { + + private lateinit var cache: Map> + + override fun get(key: Int, text: String, pageNumber: Int, filter: SearchFilter): Page { + if (MAX_PAGE <= pageNumber) { + return guildService.search( + text = text, + pageNumber = pageNumber, + filter = filter, + ) + } + + val guilds = cache[key % MAX_KEY] + ?: throw IllegalStateException("Cannot find random guild data from key \"$key\"") + + val filteredGuilds = guilds.filter { + if (text.isBlank()) { + true + } else { + it.getTitle().contains(text) or it.getBody().contains(text) + } + } + + val response = mutableListOf() + + repeat(PAGE_SIZE) { + val idx = it + pageNumber * PAGE_SIZE + + if (filteredGuilds.size <= idx) { + return@repeat + } + + response.add(filteredGuilds[idx]) + } + + return PageImpl(filter.sort(response), Pageable.ofSize(pageNumber), guilds.size.toLong()) + } + + @Scheduled(cron = ONCE_0AM_TIME) + @EventListener(ContextRefreshedEvent::class) + fun updateRandom() { + val guilds = guildService.findAllWithLimit(LIMIT) + + val updateCache = mutableMapOf>() + repeat(MAX_KEY) { updateCache[it] = guilds.shuffled() } + + cache = updateCache + } + + companion object { + private const val MAX_KEY = 3 + private const val ONCE_0AM_TIME = "0 0 0/1 * * ?" + private const val LIMIT = PAGE_SIZE * 10 + private const val MAX_PAGE = LIMIT / PAGE_SIZE + } +} From 9bb7c13ebcf079d588a250caadc0a9f5f0ff72c3 Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 18:18:51 +0900 Subject: [PATCH 13/24] =?UTF-8?q?feat:=20contribution=EC=9D=B4=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=90=98=EB=A9=B4=20Guild?= =?UTF-8?q?=EC=97=90=20=EC=86=8D=ED=95=9C=20=EC=9C=A0=EC=A0=80=EB=93=A4?= =?UTF-8?q?=EC=9D=98=20contribution=EB=8F=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gitanimals/guild/domain/Guild.kt | 8 ++++++ .../guild/domain/GuildRepository.kt | 9 +++++++ .../gitanimals/guild/domain/GuildService.kt | 7 +++++ .../org/gitanimals/guild/domain/Member.kt | 4 +++ .../UpdateGuildContributionSagaHandler.kt | 26 +++++++++++++++++++ .../saga/event/UserContributionUpdated.kt | 7 +++++ .../render/saga/VisitedSagaHandlers.kt | 2 +- .../gitanimals/render/saga/event/GavePoint.kt | 1 + 8 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/saga/UpdateGuildContributionSagaHandler.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/saga/event/UserContributionUpdated.kt diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index d767c95..66aa8ee 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -135,6 +135,14 @@ class Guild( return leader.contributions + members.sumOf { it.getContributions() } } + fun updateContributions(username: String, contributions: Long) { + if (leader.name == username) { + leader.contributions = contributions + return + } + members.firstOrNull { it.name == username }?.setContributions(contributions) + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index e65a359..3b0f19b 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -28,6 +28,15 @@ interface GuildRepository : JpaRepository { ) fun findAllGuildByUserIdWithMembers(@Param("userId") userId: String): List + @Query( + """ + select g from Guild as g + join fetch g.members as m + where g.leader.name = :username or m.name = :username + """ + ) + fun findAllGuildByUsernameWithMembers(@Param("username") username: String): List + @Query("select g from Guild as g") fun findAllWithLimit(pageable: Pageable): List diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 762c38a..343e0e9 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -92,6 +92,13 @@ class GuildService( return guild } + @Transactional + fun updateContribution(username: String, contributions: Long) { + val guilds = guildRepository.findAllGuildByUsernameWithMembers(username) + + guilds.forEach { it.updateContributions(username, contributions) } + } + fun findAllGuildByUserId(userId: String): List { return guildRepository.findAllGuildByUserIdWithMembers(userId).apply { this.forEach { loadWaitMembers.invoke(it) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index 701f90d..4bbe680 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -29,6 +29,10 @@ class Member( fun getContributions() = contributions + fun setContributions(contributions: Long) { + this.contributions = contributions + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Member) return false diff --git a/src/main/kotlin/org/gitanimals/guild/saga/UpdateGuildContributionSagaHandler.kt b/src/main/kotlin/org/gitanimals/guild/saga/UpdateGuildContributionSagaHandler.kt new file mode 100644 index 0000000..78c2b07 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/saga/UpdateGuildContributionSagaHandler.kt @@ -0,0 +1,26 @@ +package org.gitanimals.guild.saga + +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.saga.event.UserContributionUpdated +import org.rooftop.netx.api.SagaCommitEvent +import org.rooftop.netx.api.SagaCommitListener +import org.rooftop.netx.meta.SagaHandler + +@SagaHandler +class UpdateGuildContributionSagaHandler( + private val guildService: GuildService, +) { + + @SagaCommitListener( + event = UserContributionUpdated::class, + noRollbackFor = [Throwable::class], + ) + fun updateGuildContributions(sagaCommitEvent: SagaCommitEvent) { + val userContributionUpdated = sagaCommitEvent.decodeEvent(UserContributionUpdated::class) + + guildService.updateContribution( + username = userContributionUpdated.username, + contributions = userContributionUpdated.contributions.toLong(), + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/saga/event/UserContributionUpdated.kt b/src/main/kotlin/org/gitanimals/guild/saga/event/UserContributionUpdated.kt new file mode 100644 index 0000000..7258a46 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/saga/event/UserContributionUpdated.kt @@ -0,0 +1,7 @@ +package org.gitanimals.guild.saga.event + +data class UserContributionUpdated( + val username: String, + val point: Long, + val contributions: Int, +) diff --git a/src/main/kotlin/org/gitanimals/render/saga/VisitedSagaHandlers.kt b/src/main/kotlin/org/gitanimals/render/saga/VisitedSagaHandlers.kt index 4b274e1..a426f17 100644 --- a/src/main/kotlin/org/gitanimals/render/saga/VisitedSagaHandlers.kt +++ b/src/main/kotlin/org/gitanimals/render/saga/VisitedSagaHandlers.kt @@ -43,7 +43,7 @@ class VisitedSagaHandlers( val increaseContributionCount = userService.updateContributions(username, contribution) sagaJoinEvent.setNextEvent( - GavePoint(username = username, point = (increaseContributionCount * 100).toLong()) + GavePoint(username = username, point = (increaseContributionCount * 100).toLong(), contribution) ) } } diff --git a/src/main/kotlin/org/gitanimals/render/saga/event/GavePoint.kt b/src/main/kotlin/org/gitanimals/render/saga/event/GavePoint.kt index 807cda2..08e3747 100644 --- a/src/main/kotlin/org/gitanimals/render/saga/event/GavePoint.kt +++ b/src/main/kotlin/org/gitanimals/render/saga/event/GavePoint.kt @@ -3,4 +3,5 @@ package org.gitanimals.render.saga.event data class GavePoint( val username: String, val point: Long, + val contributions: Int, ) From aa183db57c11af7cb883dff718f962cf01d41445 Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 18:46:04 +0900 Subject: [PATCH 14/24] fix: compile error and test bug --- .../gitanimals/guild/app/JoinGuildFacade.kt | 6 +- .../guild/app/CreateGuildFacadeTest.kt | 16 ++--- .../{SagaCapture.kt => GuildSagaCapture.kt} | 2 +- .../render/app/UserStatisticScheduleTest.kt | 20 ++++-- .../org/gitanimals/render/controller/Api.kt | 15 ----- .../controller/filter/CorsFilterTest.kt | 63 ------------------- .../org/gitanimals/render/domain/UserTest.kt | 11 ---- .../render/saga/UsedCouponSagaHandlerTest.kt | 19 +++++- 8 files changed, 44 insertions(+), 108 deletions(-) rename src/test/kotlin/org/gitanimals/guild/supports/{SagaCapture.kt => GuildSagaCapture.kt} (98%) delete mode 100644 src/test/kotlin/org/gitanimals/render/controller/Api.kt delete mode 100644 src/test/kotlin/org/gitanimals/render/controller/filter/CorsFilterTest.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt index 4fecbc5..5c0faa9 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt @@ -56,7 +56,7 @@ class JoinGuildFacade( userId = it.userId, newUserImage = member.profileImage, newUserName = member.username, - guildTitle = guild.title, + guildTitle = guild.getTitle(), ) ) } @@ -71,7 +71,7 @@ class JoinGuildFacade( userId = guild.getLeaderId(), newUserImage = member.profileImage, newUserName = member.username, - guildTitle = guild.title, + guildTitle = guild.getTitle(), ) ) } @@ -83,7 +83,7 @@ class JoinGuildFacade( sagaManager.startSync( InboxInputEvent.sentJoinRequest( userId = member.id.toLong(), - guildTitle = guild.title, + guildTitle = guild.getTitle(), ) ) } diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt index c10eef3..07fcb5b 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -12,7 +12,7 @@ import org.gitanimals.guild.domain.GuildFarmType import org.gitanimals.guild.domain.GuildRepository import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.supports.RedisContainer -import org.gitanimals.guild.supports.SagaCapture +import org.gitanimals.guild.supports.GuildSagaCapture import org.rooftop.netx.meta.EnableSaga import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Application import org.springframework.boot.autoconfigure.domain.EntityScan @@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.seconds classes = [ Application::class, RedisContainer::class, - SagaCapture::class, + GuildSagaCapture::class, CreateGuildFacade::class, MockApiConfiguration::class, GuildService::class, @@ -40,13 +40,13 @@ import kotlin.time.Duration.Companion.seconds @EnableJpaRepositories(basePackages = ["org.gitanimals.guild"]) internal class CreateGuildFacadeTest( private val createGuildFacade: CreateGuildFacade, - private val sagaCapture: SagaCapture, + private val guildSagaCapture: GuildSagaCapture, private val identityApi: IdentityApi, private val guildRepository: GuildRepository, ) : DescribeSpec({ beforeEach { - sagaCapture.clear() + guildSagaCapture.clear() guildRepository.deleteAll() } @@ -57,7 +57,7 @@ internal class CreateGuildFacadeTest( createGuildFacade.createGuild(TOKEN, createGuildRequest) } - sagaCapture.countShouldBe( + guildSagaCapture.countShouldBe( start = 1, commit = 1, ) @@ -75,7 +75,7 @@ internal class CreateGuildFacadeTest( } eventually(5.seconds) { - sagaCapture.countShouldBe(start = 1, rollback = 1) + guildSagaCapture.countShouldBe(start = 1, rollback = 1) } } } @@ -84,14 +84,14 @@ internal class CreateGuildFacadeTest( it("유저에게 돈을 돌려준다.") { // Create Duplicate data set for throw error createGuildFacade.createGuild(TOKEN, createGuildRequest) - sagaCapture.clear() + guildSagaCapture.clear() shouldThrowAny { createGuildFacade.createGuild(TOKEN, createGuildRequest) } eventually(5.seconds) { - sagaCapture.countShouldBe( + guildSagaCapture.countShouldBe( start = 1, commit = 1, rollback = 1, diff --git a/src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt b/src/test/kotlin/org/gitanimals/guild/supports/GuildSagaCapture.kt similarity index 98% rename from src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt rename to src/test/kotlin/org/gitanimals/guild/supports/GuildSagaCapture.kt index e83643d..9b4143b 100644 --- a/src/test/kotlin/org/gitanimals/guild/supports/SagaCapture.kt +++ b/src/test/kotlin/org/gitanimals/guild/supports/GuildSagaCapture.kt @@ -5,7 +5,7 @@ import org.rooftop.netx.api.* import org.rooftop.netx.meta.SagaHandler @SagaHandler -class SagaCapture { +class GuildSagaCapture { val storage = mutableMapOf() diff --git a/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt b/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt index 086d351..819ce65 100644 --- a/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt +++ b/src/test/kotlin/org/gitanimals/render/app/UserStatisticScheduleTest.kt @@ -1,23 +1,33 @@ package org.gitanimals.render.app import io.kotest.assertions.nondeterministic.eventually +import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec -import org.gitanimals.Application +import org.gitanimals.render.domain.UserStatisticService import org.gitanimals.render.supports.RedisContainer import org.gitanimals.render.supports.SagaCapture -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.ActiveProfiles +import org.rooftop.netx.meta.EnableSaga +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource import kotlin.time.Duration.Companion.seconds -@SpringBootTest( +@EnableSaga +@DataJpaTest +@ContextConfiguration( classes = [ - Application::class, RedisContainer::class, SagaCapture::class, + UserStatisticSchedule::class, + UserStatisticService::class, ] ) @TestPropertySource("classpath:application.properties") +@DisplayName("UserStatisticSchedule 클래스의") +@EntityScan(basePackages = ["org.gitanimals.render.domain"]) +@EnableJpaRepositories(basePackages = ["org.gitanimals.render.domain"]) internal class UserStatisticScheduleTest( private val userStatisticSchedule: UserStatisticSchedule, private val sagaCapture: SagaCapture, diff --git a/src/test/kotlin/org/gitanimals/render/controller/Api.kt b/src/test/kotlin/org/gitanimals/render/controller/Api.kt deleted file mode 100644 index 3f9cf2a..0000000 --- a/src/test/kotlin/org/gitanimals/render/controller/Api.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.gitanimals.render.controller - -import io.restassured.RestAssured -import io.restassured.http.ContentType -import io.restassured.response.ExtractableResponse -import io.restassured.response.Response - -fun users(username: String): ExtractableResponse = - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .`when`().log().all() - .get("/users/$username") - .then().log().all() - .extract() diff --git a/src/test/kotlin/org/gitanimals/render/controller/filter/CorsFilterTest.kt b/src/test/kotlin/org/gitanimals/render/controller/filter/CorsFilterTest.kt deleted file mode 100644 index 8944ebe..0000000 --- a/src/test/kotlin/org/gitanimals/render/controller/filter/CorsFilterTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.gitanimals.render.controller.filter - -import io.kotest.core.spec.style.DescribeSpec -import io.kotest.matchers.collections.shouldExistInOrder -import io.restassured.RestAssured -import io.restassured.http.Header -import org.gitanimals.render.controller.users -import org.gitanimals.render.domain.User -import org.gitanimals.render.domain.UserRepository -import org.gitanimals.render.supports.RedisContainer -import org.junit.jupiter.api.DisplayName -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.test.context.ContextConfiguration - -@ContextConfiguration( - classes = [ - RedisContainer::class - ] -) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@DisplayName("Cors 적용 테스트의") -internal class CorsFilterTest( - @LocalServerPort private val port: Int, - private val userRepository: UserRepository, -) : DescribeSpec({ - - beforeSpec { - RestAssured.port = port - } - - afterEach { userRepository.deleteAll() } - - describe("/users/{username} api는") { - context("호출되면, ") { - it("cors 허용 header들을 추가해서 반환한다.") { - val user = userRepository.saveAndFlush(user) - - val response = users(user.name) - - response.headers().shouldExistInOrder( - listOf( - { it == Header("Access-Control-Allow-Origin", "*") }, - { it == Header("Access-Control-Allow-Methods", "*") }, - { it == Header("Access-Control-Max-Age", "3600") }, - { - it == Header( - "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept, Authorization, Api-Version" - ) - } - ) - ) - } - } - } - -}) { - - private companion object { - private val user = User.newUser("devxb", mutableMapOf(2024 to 1000)) - } -} diff --git a/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt b/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt index a716253..9735a2f 100644 --- a/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt +++ b/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt @@ -91,17 +91,6 @@ internal class UserTest : DescribeSpec({ user.personas.find { it.type == PersonaType.PENGUIN }.shouldNotBeNull() } } - - context("Bonus pet 목록에 등록되지 않은 pet의 이름이 주어질 경우,") { - val user = User.newUser("new-user", mutableMapOf()) - val persona = PersonaType.GOBLIN_BAG - - it("예외를 던진다.") { - shouldThrowWithMessage("Cannot select as a bonus persona.") { - user.giveNewPersonaByType(persona) - } - } - } } describe("mergePersona 메소드는") { diff --git a/src/test/kotlin/org/gitanimals/render/saga/UsedCouponSagaHandlerTest.kt b/src/test/kotlin/org/gitanimals/render/saga/UsedCouponSagaHandlerTest.kt index 5bcede6..2f946c3 100644 --- a/src/test/kotlin/org/gitanimals/render/saga/UsedCouponSagaHandlerTest.kt +++ b/src/test/kotlin/org/gitanimals/render/saga/UsedCouponSagaHandlerTest.kt @@ -8,20 +8,35 @@ import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.should import org.gitanimals.render.domain.PersonaType import org.gitanimals.render.domain.UserRepository +import org.gitanimals.render.domain.UserService import org.gitanimals.render.domain.user import org.gitanimals.render.saga.event.CouponUsed import org.gitanimals.render.supports.RedisContainer +import org.gitanimals.render.supports.SagaCapture import org.rooftop.netx.api.SagaManager -import org.springframework.boot.test.context.SpringBootTest +import org.rooftop.netx.meta.EnableSaga +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource import kotlin.time.Duration.Companion.seconds -@SpringBootTest( +@EnableSaga +@DataJpaTest +@ContextConfiguration( classes = [ RedisContainer::class, + SagaCapture::class, + UserService::class, + UsedCouponSagaHandlers::class, ] ) @DisplayName("UsedCouponSagaHandler 클래스의") +@TestPropertySource("classpath:application.properties") +@EntityScan(basePackages = ["org.gitanimals.render.domain"]) +@EnableJpaRepositories(basePackages = ["org.gitanimals.render.domain"]) internal class UsedCouponSagaHandlerTest( private val sagaManager: SagaManager, private val userRepository: UserRepository, From 7a4560024100ddcbd4b48d767c995a6bcf64a56b Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 22:30:39 +0900 Subject: [PATCH 15/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=A0=20=EB=95=8C=20leader=EC=9D=98=20per?= =?UTF-8?q?sonaId=EB=A5=BC=20=EC=84=A0=ED=83=9D=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt | 3 ++- .../org/gitanimals/guild/app/request/CreateGuildRequest.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt index 95c72b1..0eb4869 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt @@ -75,7 +75,8 @@ class CreateGuildFacade( val createLeaderRequest = CreateLeaderRequest( userId = leader.id.toLong(), name = leader.username, - personaId = renderUser.personas.maxBy { it.level }.id.toLong(), + personaId = renderUser.personas.firstOrNull { it.id == createGuildRequest.personaId }?.id?.toLong() + ?: throw IllegalArgumentException("Cannot find persona by id \"${createGuildRequest.personaId}\""), contributions = renderUser.totalContributions.toLong(), ) diff --git a/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt b/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt index e7b604b..33cc584 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/request/CreateGuildRequest.kt @@ -8,4 +8,5 @@ data class CreateGuildRequest( val guildIcon: String, val autoJoin: Boolean, val farmType: GuildFarmType, + val personaId: String, ) From 1fa2f306f665bce0a9ff06d697b11de1b3668220 Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 23:35:51 +0900 Subject: [PATCH 16/24] =?UTF-8?q?feat:=20persona=EA=B0=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=90=98=EB=A9=B4=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gitanimals/render/domain/User.kt | 41 +++++++++++-------- .../render/domain/event/PersonaDeleted.kt | 11 +++++ .../domain/listeners/DomainEventPublisher.kt | 21 ++++++++++ .../render/infra/CustomExecutorConfigurer.kt | 28 +++++++++++++ .../infra/PersonaDeletedEventHandler.kt | 20 +++++++++ .../render/domain/UserServiceTest.kt | 14 +++---- .../org/gitanimals/render/domain/UserTest.kt | 37 ++++++++++++++++- .../render/supports/DomainEventHolder.kt | 24 +++++++++++ .../render/supports/IntegrationTest.kt | 19 +++++++++ 9 files changed, 189 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/render/domain/event/PersonaDeleted.kt create mode 100644 src/main/kotlin/org/gitanimals/render/domain/listeners/DomainEventPublisher.kt create mode 100644 src/main/kotlin/org/gitanimals/render/infra/CustomExecutorConfigurer.kt create mode 100644 src/main/kotlin/org/gitanimals/render/infra/PersonaDeletedEventHandler.kt create mode 100644 src/test/kotlin/org/gitanimals/render/supports/DomainEventHolder.kt create mode 100644 src/test/kotlin/org/gitanimals/render/supports/IntegrationTest.kt diff --git a/src/main/kotlin/org/gitanimals/render/domain/User.kt b/src/main/kotlin/org/gitanimals/render/domain/User.kt index 8db5c0d..94e83af 100644 --- a/src/main/kotlin/org/gitanimals/render/domain/User.kt +++ b/src/main/kotlin/org/gitanimals/render/domain/User.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.* import org.gitanimals.render.core.AggregateRoot import org.gitanimals.render.core.IdGenerator +import org.gitanimals.render.domain.event.PersonaDeleted +import org.gitanimals.render.domain.listeners.DomainEventPublisher import org.gitanimals.render.domain.response.PersonaResponse import org.gitanimals.render.domain.value.Contribution import org.gitanimals.render.domain.value.Level @@ -68,11 +70,33 @@ class User( return PersonaResponse.from(persona) } + fun mergePersona(increasePersonaId: Long, deletePersonaId: Long): Persona { + require(increasePersonaId != deletePersonaId) { + "increasePersonaId \"$increasePersonaId\", deletePersonaId \"$deletePersonaId\" must be different" + } + + val increasePersona = personas.first { it.id == increasePersonaId } + val deletePersona = personas.first { it.id == deletePersonaId } + + increasePersona.level.value += max(deletePersona.level.value / 2, 1) + + deletePersona(deletePersona.id) + + return increasePersona + } + fun deletePersona(personaId: Long): PersonaResponse { val persona = this.personas.find { it.id == personaId } ?: throw IllegalArgumentException("Cannot find persona by id \"$personaId\"") this.personas.remove(persona) + DomainEventPublisher.publish( + PersonaDeleted( + userId = id, + username = name, + personaId = personaId, + ) + ) return PersonaResponse.from(persona) } @@ -178,6 +202,7 @@ class User( return this.append("") } + fun createFarmAnimation(): String { val field = getOrCreateDefaultFieldIfAbsent() @@ -192,7 +217,6 @@ class User( .closeSvg() } - fun contributionCount(): Long = contributions.totalCount() fun changeField(fieldType: FieldType) { @@ -247,21 +271,6 @@ class User( .append("") .toString() - fun mergePersona(increasePersonaId: Long, deletePersonaId: Long): Persona { - require(increasePersonaId != deletePersonaId) { - "increasePersonaId \"$increasePersonaId\", deletePersonaId \"$deletePersonaId\" must be different" - } - - val increasePersona = personas.first { it.id == increasePersonaId } - val deletePersona = personas.first { it.id == deletePersonaId } - - increasePersona.level.value += max(deletePersona.level.value / 2, 1) - - personas.remove(deletePersona) - - return increasePersona - } - companion object { private const val MAX_PERSONA_COUNT = 30L private const val MAX_INIT_PERSONA_COUNT = 10L diff --git a/src/main/kotlin/org/gitanimals/render/domain/event/PersonaDeleted.kt b/src/main/kotlin/org/gitanimals/render/domain/event/PersonaDeleted.kt new file mode 100644 index 0000000..4913b2e --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/domain/event/PersonaDeleted.kt @@ -0,0 +1,11 @@ +package org.gitanimals.render.domain.event + +import org.gitanimals.render.core.clock +import java.time.Instant + +data class PersonaDeleted( + val userId: Long, + val username: String, + val personaId: Long, + val personaDeletedAt: Instant = clock.instant(), +) diff --git a/src/main/kotlin/org/gitanimals/render/domain/listeners/DomainEventPublisher.kt b/src/main/kotlin/org/gitanimals/render/domain/listeners/DomainEventPublisher.kt new file mode 100644 index 0000000..77430d7 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/domain/listeners/DomainEventPublisher.kt @@ -0,0 +1,21 @@ +package org.gitanimals.render.domain.listeners + +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Component + +object DomainEventPublisher { + + private lateinit var applicationEventPublisher: ApplicationEventPublisher + + fun publish(event: T) { + applicationEventPublisher.publishEvent(event) + } + + @Component + class EventPublisherInjector(applicationEventPublisher: ApplicationEventPublisher) { + + init { + DomainEventPublisher.applicationEventPublisher = applicationEventPublisher + } + } +} diff --git a/src/main/kotlin/org/gitanimals/render/infra/CustomExecutorConfigurer.kt b/src/main/kotlin/org/gitanimals/render/infra/CustomExecutorConfigurer.kt new file mode 100644 index 0000000..601264f --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/infra/CustomExecutorConfigurer.kt @@ -0,0 +1,28 @@ +package org.gitanimals.render.infra + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor +import java.util.concurrent.Executor +import kotlin.time.Duration.Companion.minutes + +@Configuration +class CustomExecutorConfigurer { + + @Bean(GRACEFUL_SHUTDOWN_EXECUTOR) + fun taskExecutor(): Executor { + val executor = ThreadPoolTaskExecutor() + executor.corePoolSize = 2 + executor.maxPoolSize = 20 + executor.threadNamePrefix = "$GRACEFUL_SHUTDOWN_EXECUTOR-" + executor.setWaitForTasksToCompleteOnShutdown(true) + executor.setAwaitTerminationSeconds(2.minutes.inWholeSeconds.toInt()) + executor.initialize() + + return executor + } + + companion object { + const val GRACEFUL_SHUTDOWN_EXECUTOR = "gracefulShutdownExecutor" + } +} diff --git a/src/main/kotlin/org/gitanimals/render/infra/PersonaDeletedEventHandler.kt b/src/main/kotlin/org/gitanimals/render/infra/PersonaDeletedEventHandler.kt new file mode 100644 index 0000000..cc96993 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/render/infra/PersonaDeletedEventHandler.kt @@ -0,0 +1,20 @@ +package org.gitanimals.render.infra + +import org.gitanimals.render.domain.event.PersonaDeleted +import org.gitanimals.render.infra.CustomExecutorConfigurer.Companion.GRACEFUL_SHUTDOWN_EXECUTOR +import org.rooftop.netx.api.SagaManager +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component + +@Component +class PersonaDeletedEventHandler( + private val sagaManager: SagaManager, +) { + + @Async(GRACEFUL_SHUTDOWN_EXECUTOR) + @EventListener + fun handlePersonaDeletedEvent(personaDeleted: PersonaDeleted) { + sagaManager.startSync(personaDeleted) + } +} diff --git a/src/test/kotlin/org/gitanimals/render/domain/UserServiceTest.kt b/src/test/kotlin/org/gitanimals/render/domain/UserServiceTest.kt index 15ff978..ffebd4d 100644 --- a/src/test/kotlin/org/gitanimals/render/domain/UserServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/render/domain/UserServiceTest.kt @@ -3,20 +3,16 @@ package org.gitanimals.render.domain import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.nulls.shouldBeNull -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ContextConfiguration +import org.gitanimals.render.domain.listeners.DomainEventPublisher +import org.gitanimals.render.supports.IntegrationTest -@DataJpaTest -@DisplayName("UserService 클래스의") -@ContextConfiguration( +@IntegrationTest( classes = [ UserService::class, + DomainEventPublisher.EventPublisherInjector::class, ] ) -@EntityScan(basePackages = ["org.gitanimals.render.domain"]) -@EnableJpaRepositories(basePackages = ["org.gitanimals.render.domain"]) +@DisplayName("UserService 클래스의") internal class UserServiceTest( private val userService: UserService, private val userRepository: UserRepository, diff --git a/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt b/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt index 9735a2f..2eb0273 100644 --- a/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt +++ b/src/test/kotlin/org/gitanimals/render/domain/UserTest.kt @@ -6,12 +6,33 @@ import io.kotest.core.annotation.DisplayName import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.nulls.shouldNotBeNull +import org.gitanimals.render.domain.event.PersonaDeleted +import org.gitanimals.render.domain.listeners.DomainEventPublisher import org.gitanimals.render.domain.value.Contribution +import org.gitanimals.render.supports.DomainEventHolder +import org.springframework.test.context.ContextConfiguration import java.time.Instant import java.time.temporal.ChronoUnit +@ContextConfiguration( + classes = [ + DomainEventHolder::class, + DomainEventPublisher.EventPublisherInjector::class, + ] +) @DisplayName("User 클래스의") -internal class UserTest : DescribeSpec({ +internal class UserTest( + /** + * 빈 주입 순서로 인해서 주입을 받아야한다. + * 그러지 않으면 lazy initialize 에러가 발생한다. + */ + eventPublisherInjector: DomainEventPublisher.EventPublisherInjector, + private val domainEventHolder: DomainEventHolder, +) : DescribeSpec({ + + beforeEach { + domainEventHolder.deleteAll() + } describe("newUser 메소드는") { context("이름에 [대문자, -, 소문자, 숫자]로 이루어진 문장이 들어올 경우") { @@ -105,6 +126,20 @@ internal class UserTest : DescribeSpec({ user.mergePersona(increasePersonaId, deletePersonaId) user.personas.size shouldBeEqual 1 + domainEventHolder.eventsShouldBe(PersonaDeleted::class, 1) + } + } + } + + describe("deletePersona 메소드는") { + context("personaId를 받으면,") { + val user = User.newUser("devxb", mapOf()) + val personaId = user.personas[0].id + + it("persona를 삭제하고 PersonaDeleted 이벤트를 발행한다.") { + user.deletePersona(personaId) + + domainEventHolder.eventsShouldBe(PersonaDeleted::class, 1) } } } diff --git a/src/test/kotlin/org/gitanimals/render/supports/DomainEventHolder.kt b/src/test/kotlin/org/gitanimals/render/supports/DomainEventHolder.kt new file mode 100644 index 0000000..a2471af --- /dev/null +++ b/src/test/kotlin/org/gitanimals/render/supports/DomainEventHolder.kt @@ -0,0 +1,24 @@ +package org.gitanimals.render.supports + +import io.kotest.matchers.shouldBe +import org.gitanimals.render.domain.event.PersonaDeleted +import org.springframework.boot.test.context.TestComponent +import org.springframework.context.event.EventListener +import kotlin.reflect.KClass + +@TestComponent +class DomainEventHolder { + + private val events = mutableMapOf, Int>() + + @EventListener(PersonaDeleted::class) + fun handlePersonaDeleted(personaDeleted: PersonaDeleted) { + events[personaDeleted::class] = events.getOrDefault(personaDeleted::class, 0) + 1 + } + + fun eventsShouldBe(kClass: KClass<*>, count: Int) { + events[kClass] shouldBe count + } + + fun deleteAll() = events.clear() +} diff --git a/src/test/kotlin/org/gitanimals/render/supports/IntegrationTest.kt b/src/test/kotlin/org/gitanimals/render/supports/IntegrationTest.kt new file mode 100644 index 0000000..3ad318f --- /dev/null +++ b/src/test/kotlin/org/gitanimals/render/supports/IntegrationTest.kt @@ -0,0 +1,19 @@ +package org.gitanimals.render.supports + +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.core.annotation.AliasFor +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.event.RecordApplicationEvents +import kotlin.reflect.KClass + +@DataJpaTest +@ContextConfiguration +@RecordApplicationEvents +@EntityScan(basePackages = ["org.gitanimals.render.domain"]) +@EnableJpaRepositories(basePackages = ["org.gitanimals.render.domain"]) +annotation class IntegrationTest( + @get:AliasFor(annotation = ContextConfiguration::class, value = "classes") + val classes: Array>, +) From cb50013ef594b3cb5ed491bf7d7bf981b689c0b0 Mon Sep 17 00:00:00 2001 From: devxb Date: Sun, 22 Dec 2024 23:36:06 +0900 Subject: [PATCH 17/24] =?UTF-8?q?fix:=20CreateGuildFacadeTest=EC=9D=98=20?= =?UTF-8?q?=EC=BB=B4=ED=8C=8C=EC=9D=BC=20=EC=98=A4=EB=A5=98=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt index 07fcb5b..8b5bdc5 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -109,7 +109,8 @@ internal class CreateGuildFacadeTest( body = "We are gitanimals", guildIcon = "gitanimals.org", autoJoin = true, - farmType = GuildFarmType.DUMMY + farmType = GuildFarmType.DUMMY, + personaId = "123456789", ) private val poolIdentityUserResponse = IdentityApi.UserResponse( From 9fd1875e85784bdfc1034f81689cb91142dc3d62 Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 00:30:21 +0900 Subject: [PATCH 18/24] =?UTF-8?q?feat:=20Persona=EA=B0=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EB=90=98=EB=A9=B4=20=EA=B8=B8=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=9C=20=EB=8C=80=ED=91=9C=ED=8E=AB?= =?UTF-8?q?=EC=9D=B4=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/app/GetJoinedGuildFacade.kt | 2 +- .../controller/response/GuildResponse.kt | 2 + .../org/gitanimals/guild/domain/Guild.kt | 19 +++ .../guild/domain/GuildRepository.kt | 6 +- .../gitanimals/guild/domain/GuildService.kt | 31 +++-- .../org/gitanimals/guild/domain/Leader.kt | 2 +- .../org/gitanimals/guild/domain/Member.kt | 2 +- .../guild/saga/PersonaDeletedSagaHandler.kt | 35 ++++++ .../guild/saga/event/PersonaDeleted.kt | 11 ++ .../guild/app/CreateGuildFacadeTest.kt | 1 + .../saga/PersonaDeletedSagaHandlerTest.kt | 117 ++++++++++++++++++ .../{app => supports}/MockApiConfiguration.kt | 4 +- 12 files changed, 212 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/saga/event/PersonaDeleted.kt create mode 100644 src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt rename src/test/kotlin/org/gitanimals/guild/{app => supports}/MockApiConfiguration.kt (92%) diff --git a/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt index a21e5f2..d118424 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/GetJoinedGuildFacade.kt @@ -13,6 +13,6 @@ class GetJoinedGuildFacade( fun getJoinedGuilds(token: String): List { val user = identityApi.getUserByToken(token) - return guildService.findAllGuildByUserId(user.id) + return guildService.findAllGuildByUserId(user.id.toLong()) } } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt index aa0e70b..a842880 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt @@ -25,6 +25,7 @@ data class GuildResponse( val userId: String, val name: String, val contributions: String, + val personaId: String, ) data class Member( @@ -55,6 +56,7 @@ data class GuildResponse( userId = guild.getLeaderId().toString(), name = guild.getLeaderName(), contributions = guild.getContributions().toString(), + personaId = guild.getLeaderPersonaId().toString(), ), farmType = guild.getGuildFarmType().toString(), totalContributions = guild.getTotalContributions().toString(), diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 66aa8ee..c53b880 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -143,6 +143,25 @@ class Guild( members.firstOrNull { it.name == username }?.setContributions(contributions) } + fun changePersonaIfDeleted(userId: Long, deletedPersonaId: Long, personaId: Long) { + if (leader.userId == userId) { + if (leader.personaId == deletedPersonaId) { + leader.personaId = personaId + } + return + } + + members.firstOrNull { + it.userId == userId && it.personaId == deletedPersonaId + }?.let { + it.personaId = personaId + } + } + + fun getLeaderPersonaId(): Long { + return leader.personaId + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt index 3b0f19b..3d85930 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildRepository.kt @@ -22,16 +22,16 @@ interface GuildRepository : JpaRepository { @Query( """ select g from Guild as g - join fetch g.members as m + left join fetch g.members as m where g.leader.userId = :userId or m.userId = :userId """ ) - fun findAllGuildByUserIdWithMembers(@Param("userId") userId: String): List + fun findAllGuildByUserIdWithMembers(@Param("userId") userId: Long): List @Query( """ select g from Guild as g - join fetch g.members as m + left join fetch g.members as m where g.leader.name = :username or m.name = :username """ ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 343e0e9..c07f1da 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -99,27 +99,32 @@ class GuildService( guilds.forEach { it.updateContributions(username, contributions) } } - fun findAllGuildByUserId(userId: String): List { - return guildRepository.findAllGuildByUserIdWithMembers(userId).apply { - this.forEach { loadWaitMembers.invoke(it) } + fun findAllGuildByUserId(userId: Long): List { + return guildRepository.findAllGuildByUserIdWithMembers(userId).onEach { + loadWaitMembers.invoke(it) } } fun search(text: String, pageNumber: Int, filter: SearchFilter): Page { - return guildRepository.search(text, Pageable.ofSize(PAGE_SIZE).withPage(pageNumber)).apply { - this.forEach { - loadMembers.invoke(it) - loadWaitMembers.invoke(it) - } + return guildRepository.search(text, Pageable.ofSize(PAGE_SIZE).withPage(pageNumber)).onEach { + loadMembers.invoke(it) + loadWaitMembers.invoke(it) } } fun findAllWithLimit(limit: Int): List { - return guildRepository.findAllWithLimit(Pageable.ofSize(limit)).apply { - this.forEach { - loadMembers.invoke(it) - loadWaitMembers.invoke(it) - } + return guildRepository.findAllWithLimit(Pageable.ofSize(limit)).onEach { + loadMembers.invoke(it) + loadWaitMembers.invoke(it) + } + } + + @Transactional + fun deletePersonaSync(userId: Long, deletedPersonaId: Long, personaId: Long) { + val guilds = guildRepository.findAllGuildByUserIdWithMembers(userId) + + guilds.forEach { + it.changePersonaIfDeleted(userId, deletedPersonaId, personaId) } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt index 61f7b8f..d419b3f 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt @@ -12,7 +12,7 @@ data class Leader( val name: String, @Column(name = "persona_id", nullable = false) - val personaId: Long, + var personaId: Long, @Column(name = "contributions", nullable = false) var contributions: Long, diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index 4bbe680..ad46252 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -17,7 +17,7 @@ class Member( val name: String, @Column(name = "persona_id", nullable = false) - val personaId: Long, + var personaId: Long, @Column(name = "contributions", nullable = false) private var contributions: Long, diff --git a/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt b/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt new file mode 100644 index 0000000..58a7ec3 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt @@ -0,0 +1,35 @@ +package org.gitanimals.guild.saga + +import org.gitanimals.guild.app.RenderApi +import org.gitanimals.guild.domain.GuildService +import org.gitanimals.guild.saga.event.PersonaDeleted +import org.rooftop.netx.api.SagaStartEvent +import org.rooftop.netx.api.SagaStartListener +import org.rooftop.netx.api.SuccessWith +import org.rooftop.netx.meta.SagaHandler + +@SagaHandler +class PersonaDeletedSagaHandler( + private val guildService: GuildService, + private val renderApi: RenderApi, +) { + + @SagaStartListener( + event = PersonaDeleted::class, + noRollbackFor = [IllegalStateException::class], + successWith = SuccessWith.END, + ) + fun handlePersonaDeletedEvent(sagaStartEvent: SagaStartEvent) { + val personaDeleted = sagaStartEvent.decodeEvent(PersonaDeleted::class) + + val personaId = + renderApi.getUserByName(personaDeleted.username).personas.maxByOrNull { it.level }?.id + ?: throw IllegalStateException("Cannot find any persona by username \"${personaDeleted.username}\"") + + guildService.deletePersonaSync( + userId = personaDeleted.userId, + deletedPersonaId = personaDeleted.personaId, + personaId = personaId.toLong(), + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/saga/event/PersonaDeleted.kt b/src/main/kotlin/org/gitanimals/guild/saga/event/PersonaDeleted.kt new file mode 100644 index 0000000..0c5f665 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/saga/event/PersonaDeleted.kt @@ -0,0 +1,11 @@ +package org.gitanimals.guild.saga.event + +import org.gitanimals.render.core.clock +import java.time.Instant + +data class PersonaDeleted( + val userId: Long, + val username: String, + val personaId: Long, + val personaDeletedAt: Instant = clock.instant(), +) diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt index 8b5bdc5..9d90e21 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -13,6 +13,7 @@ import org.gitanimals.guild.domain.GuildRepository import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.supports.RedisContainer import org.gitanimals.guild.supports.GuildSagaCapture +import org.gitanimals.guild.supports.MockApiConfiguration import org.rooftop.netx.meta.EnableSaga import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Application import org.springframework.boot.autoconfigure.domain.EntityScan diff --git a/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt b/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt new file mode 100644 index 0000000..69c4518 --- /dev/null +++ b/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt @@ -0,0 +1,117 @@ +package org.gitanimals.guild.saga + +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.gitanimals.guild.app.RenderApi +import org.gitanimals.guild.domain.* +import org.gitanimals.guild.saga.event.PersonaDeleted +import org.gitanimals.guild.supports.MockApiConfiguration +import org.gitanimals.render.supports.RedisContainer +import org.rooftop.netx.api.SagaManager +import org.rooftop.netx.meta.EnableSaga +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import kotlin.time.Duration.Companion.seconds + +@EnableSaga +@DataJpaTest +@ContextConfiguration( + classes = [ + RedisContainer::class, + GuildService::class, + MockApiConfiguration::class, + PersonaDeletedSagaHandler::class, + ] +) +@EntityScan(basePackages = ["org.gitanimals.guild"]) +@EnableJpaRepositories(basePackages = ["org.gitanimals.guild"]) +@DisplayName("PersonaDeletedSagaHandler 클래스의") +@TestPropertySource("classpath:application.properties") +internal class PersonaDeletedSagaHandlerTest( + private val renderApi: RenderApi, + private val sagaManager: SagaManager, + private val guildRepository: GuildRepository, +) : DescribeSpec({ + describe("handlePersonaDeletedEvent 메소드는") { + context("PersonaDeletedEvent를 받으면,") { + val leaderId = 999L + val leaderPersonaId = 100L + val leaderName = "devxb" + var guild = guild( + leader = leader( + userId = leaderId, + name = "devxb", + personaId = leaderPersonaId + ) + ) + + val memberId = 200L + val memberPersonaId = 200L + val memberName = "member" + guild.join( + memberUserId = memberId, + memberName = memberName, + memberPersonaId = memberPersonaId, + memberContributions = 100L, + ) + guild = guildRepository.save(guild) + + val changePersonaId = 101L + val leaderPersonaDeleted = PersonaDeleted( + userId = leaderId, + username = leaderName, + personaId = leaderPersonaId, + ) + + val memberPersonaDeleted = PersonaDeleted( + userId = memberId, + username = memberName, + personaId = memberPersonaId, + ) + + every { renderApi.getUserByName(leaderName) } returns RenderApi.UserResponse( + id = "1", + name = "devxb", + totalContributions = "1", + personas = listOf( + RenderApi.UserResponse.PersonaResponse(changePersonaId.toString(), "10") + ) + ) + + every { renderApi.getUserByName(memberName) } returns RenderApi.UserResponse( + id = "1", + name = "member", + totalContributions = "1", + personas = listOf( + RenderApi.UserResponse.PersonaResponse(changePersonaId.toString(), "10") + ) + ) + + it("리더의 삭제된 펫을 모두 찾아 새로운 펫으로 변경한다.") { + sagaManager.startSync(leaderPersonaDeleted) + + eventually(5.seconds) { + guildRepository.findByIdOrNull(guild.id) + ?.getLeaderPersonaId() shouldBe changePersonaId + } + } + + it("멤버의 삭제된 펫을 모두 찾아 새로운 펫으로 변경한다.") { + sagaManager.startSync(memberPersonaDeleted) + + eventually(5.seconds) { + guildRepository.findAllGuildByUserIdWithMembers(memberId)[0] + .getMembers() + .first { it.userId == memberId }.personaId shouldBe changePersonaId + } + } + } + } +}) diff --git a/src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt b/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt similarity index 92% rename from src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt rename to src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt index 0dee383..66b82a9 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/MockApiConfiguration.kt +++ b/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt @@ -1,9 +1,11 @@ -package org.gitanimals.guild.app +package org.gitanimals.guild.supports import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk +import org.gitanimals.guild.app.IdentityApi +import org.gitanimals.guild.app.RenderApi import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean From 5a21e0227f8f450ff4b4eeb39557debb2db0be18 Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 00:47:30 +0900 Subject: [PATCH 19/24] =?UTF-8?q?feat:=20Member,=20Leader,=20WaitMember?= =?UTF-8?q?=EA=B0=80=20personaType=EB=8F=84=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EA=B0=96=EA=B3=A0=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gitanimals/guild/app/CreateGuildFacade.kt | 1 + .../gitanimals/guild/app/JoinGuildFacade.kt | 1 + .../org/gitanimals/guild/app/RenderApi.kt | 1 + .../org/gitanimals/guild/domain/Guild.kt | 16 +++++++++--- .../gitanimals/guild/domain/GuildService.kt | 25 ++++++++++++++----- .../org/gitanimals/guild/domain/Leader.kt | 3 +++ .../org/gitanimals/guild/domain/Member.kt | 5 ++++ .../org/gitanimals/guild/domain/WaitMember.kt | 6 +++++ .../domain/request/CreateLeaderRequest.kt | 2 ++ .../guild/saga/PersonaDeletedSagaHandler.kt | 7 +++--- .../guild/app/CreateGuildFacadeTest.kt | 2 +- .../org/gitanimals/guild/domain/Fixture.kt | 10 +++++--- .../guild/domain/GuildServiceTest.kt | 5 ++++ .../saga/PersonaDeletedSagaHandlerTest.kt | 13 ++++++++-- .../guild/supports/MockApiConfiguration.kt | 6 +++-- 15 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt index 0eb4869..d802527 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/CreateGuildFacade.kt @@ -78,6 +78,7 @@ class CreateGuildFacade( personaId = renderUser.personas.firstOrNull { it.id == createGuildRequest.personaId }?.id?.toLong() ?: throw IllegalArgumentException("Cannot find persona by id \"${createGuildRequest.personaId}\""), contributions = renderUser.totalContributions.toLong(), + personaType = renderUser.personas.find { it.id == createGuildRequest.personaId }!!.type, ) guildService.createGuild( diff --git a/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt index 5c0faa9..91f4397 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/JoinGuildFacade.kt @@ -32,6 +32,7 @@ class JoinGuildFacade( memberName = member.username, memberPersonaId = memberPersonaId, memberContributions = renderInfo.totalContributions.toLong(), + memberPersonaType = renderInfo.personas.find { it.id.toLong() == memberPersonaId }!!.type, ) val guild = guildService.getGuildById(guildId) diff --git a/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt b/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt index e254108..9052785 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/RenderApi.kt @@ -18,6 +18,7 @@ fun interface RenderApi { data class PersonaResponse( val id: String, val level: String, + val type: String, ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index c53b880..1e65a1f 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -71,6 +71,7 @@ class Guild( memberName: String, memberPersonaId: Long, memberContributions: Long, + memberPersonaType: String, ) { require(leader.userId != memberUserId) { "Leader cannot join their own guild leaderId: \"${leader.userId}\", memberUserId: \"$memberUserId\"" @@ -83,6 +84,7 @@ class Guild( name = memberName, personaId = memberPersonaId, contributions = memberContributions, + personaType = memberPersonaType, ) members.add(member) return @@ -94,6 +96,7 @@ class Guild( name = memberName, personaId = memberPersonaId, contributions = memberContributions, + personaType = memberPersonaType, ) waitMembers.add(waitMember) } @@ -143,10 +146,16 @@ class Guild( members.firstOrNull { it.name == username }?.setContributions(contributions) } - fun changePersonaIfDeleted(userId: Long, deletedPersonaId: Long, personaId: Long) { + fun changePersonaIfDeleted( + userId: Long, + deletedPersonaId: Long, + changePersonaId: Long, + changePersonaType: String, + ) { if (leader.userId == userId) { if (leader.personaId == deletedPersonaId) { - leader.personaId = personaId + leader.personaId = changePersonaId + leader.personaType = changePersonaType } return } @@ -154,7 +163,8 @@ class Guild( members.firstOrNull { it.userId == userId && it.personaId == deletedPersonaId }?.let { - it.personaId = personaId + it.personaId = changePersonaId + it.personaType = changePersonaType } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index c07f1da..116c8b8 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -48,6 +48,7 @@ class GuildService( memberUserId: Long, memberName: String, memberPersonaId: Long, + memberPersonaType: String, memberContributions: Long, ) { val guild = getGuildById(guildId) @@ -57,6 +58,7 @@ class GuildService( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = memberContributions, + memberPersonaType = memberPersonaType, ) } @@ -106,10 +108,11 @@ class GuildService( } fun search(text: String, pageNumber: Int, filter: SearchFilter): Page { - return guildRepository.search(text, Pageable.ofSize(PAGE_SIZE).withPage(pageNumber)).onEach { - loadMembers.invoke(it) - loadWaitMembers.invoke(it) - } + return guildRepository.search(text, Pageable.ofSize(PAGE_SIZE).withPage(pageNumber)) + .onEach { + loadMembers.invoke(it) + loadWaitMembers.invoke(it) + } } fun findAllWithLimit(limit: Int): List { @@ -120,11 +123,21 @@ class GuildService( } @Transactional - fun deletePersonaSync(userId: Long, deletedPersonaId: Long, personaId: Long) { + fun deletePersonaSync( + userId: Long, + deletedPersonaId: Long, + changePersonaId: Long, + changePersonaType: String, + ) { val guilds = guildRepository.findAllGuildByUserIdWithMembers(userId) guilds.forEach { - it.changePersonaIfDeleted(userId, deletedPersonaId, personaId) + it.changePersonaIfDeleted( + userId = userId, + deletedPersonaId = deletedPersonaId, + changePersonaId = changePersonaId, + changePersonaType = changePersonaType, + ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt index d419b3f..d27d0ff 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Leader.kt @@ -14,6 +14,9 @@ data class Leader( @Column(name = "persona_id", nullable = false) var personaId: Long, + @Column(name = "persona_type", nullable = false) + var personaType: String, + @Column(name = "contributions", nullable = false) var contributions: Long, ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt index ad46252..6aeec67 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Member.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Member.kt @@ -19,6 +19,9 @@ class Member( @Column(name = "persona_id", nullable = false) var personaId: Long, + @Column(name = "persona_type", nullable = false) + var personaType: String, + @Column(name = "contributions", nullable = false) private var contributions: Long, @@ -51,6 +54,7 @@ class Member( userId: Long, name: String, personaId: Long, + personaType: String, contributions: Long, ): Member { return Member( @@ -58,6 +62,7 @@ class Member( userId = userId, name = name, personaId = personaId, + personaType = personaType, guild = guild, contributions = contributions, ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt index 2c1212c..77ad036 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt @@ -27,6 +27,9 @@ class WaitMember( @Column(name = "persona_id", nullable = false) val personaId: Long, + @Column(name = "persona_type", nullable = false) + var personaType: String, + @Column(name = "contributions", nullable = false) private var contributions: Long, @@ -43,6 +46,7 @@ class WaitMember( personaId = personaId, guild = guild, contributions = contributions, + personaType = personaType, ) override fun equals(other: Any?): Boolean { @@ -62,6 +66,7 @@ class WaitMember( userId: Long, name: String, personaId: Long, + personaType: String, contributions: Long, ): WaitMember { return WaitMember( @@ -69,6 +74,7 @@ class WaitMember( userId = userId, name = name, personaId = personaId, + personaType = personaType, guild = guild, contributions = contributions, ) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt b/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt index 951944e..a2d4199 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/request/CreateLeaderRequest.kt @@ -7,6 +7,7 @@ data class CreateLeaderRequest( val name: String, val personaId: Long, val contributions: Long, + val personaType: String, ) { fun toDomain(): Leader { @@ -15,6 +16,7 @@ data class CreateLeaderRequest( name = name, personaId = personaId, contributions = contributions, + personaType = personaType, ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt b/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt index 58a7ec3..4dcb16a 100644 --- a/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt +++ b/src/main/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandler.kt @@ -22,14 +22,15 @@ class PersonaDeletedSagaHandler( fun handlePersonaDeletedEvent(sagaStartEvent: SagaStartEvent) { val personaDeleted = sagaStartEvent.decodeEvent(PersonaDeleted::class) - val personaId = - renderApi.getUserByName(personaDeleted.username).personas.maxByOrNull { it.level }?.id + val changePersona = + renderApi.getUserByName(personaDeleted.username).personas.maxByOrNull { it.level } ?: throw IllegalStateException("Cannot find any persona by username \"${personaDeleted.username}\"") guildService.deletePersonaSync( userId = personaDeleted.userId, deletedPersonaId = personaDeleted.personaId, - personaId = personaId.toLong(), + changePersonaId = changePersona.id.toLong(), + changePersonaType = changePersona.type, ) } } diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt index 9d90e21..abf442f 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -111,7 +111,7 @@ internal class CreateGuildFacadeTest( guildIcon = "gitanimals.org", autoJoin = true, farmType = GuildFarmType.DUMMY, - personaId = "123456789", + personaId = "3", ) private val poolIdentityUserResponse = IdentityApi.UserResponse( diff --git a/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt index a72ca28..bc10d85 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/Fixture.kt @@ -29,12 +29,14 @@ fun leader( name: String = "Default Leader", personaId: Long = 1L, contributions: Long = 0L, + personaType: String = "GOOSE", ): Leader { return Leader( userId = userId, name = name, personaId = personaId, - contributions = contributions + contributions = contributions, + personaType = personaType, ) } @@ -43,13 +45,15 @@ fun member( userId: Long = 2L, name: String = "DefaultName", personaId: Long = 200L, - contributions: Long = 500L + contributions: Long = 500L, + personaType: String = "GOOSE", ): Member { return Member.create( guild = guild, userId = userId, name = name, personaId = personaId, - contributions = contributions + contributions = contributions, + personaType = personaType, ) } diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt index 5060f17..afc152d 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -38,6 +38,7 @@ internal class GuildServiceTest( name = "devxb", personaId = 2L, contributions = 3L, + personaType = "GOOSE", ) it("중복된 길드가 아니라면 길드를 생성한다.") { @@ -94,6 +95,7 @@ internal class GuildServiceTest( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = memberContributions, + memberPersonaType = "GOOSE", ) guildService.getGuildById(guild.id, loadMembers).getMembers().size shouldBe 1 @@ -115,6 +117,7 @@ internal class GuildServiceTest( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = memberContributions, + memberPersonaType = "GOOSE", ) guildService.getGuildById(guild.id, loadWaitMembers) @@ -137,6 +140,7 @@ internal class GuildServiceTest( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = memberContributions, + memberPersonaType = "GOOSE", ) } } @@ -157,6 +161,7 @@ internal class GuildServiceTest( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = memberContributions, + memberPersonaType = "GOOSE", ) it("멤버를 가입시킨다.") { diff --git a/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt b/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt index 69c4518..e246d26 100644 --- a/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/saga/PersonaDeletedSagaHandlerTest.kt @@ -60,6 +60,7 @@ internal class PersonaDeletedSagaHandlerTest( memberName = memberName, memberPersonaId = memberPersonaId, memberContributions = 100L, + memberPersonaType = "GOOSE", ) guild = guildRepository.save(guild) @@ -81,7 +82,11 @@ internal class PersonaDeletedSagaHandlerTest( name = "devxb", totalContributions = "1", personas = listOf( - RenderApi.UserResponse.PersonaResponse(changePersonaId.toString(), "10") + RenderApi.UserResponse.PersonaResponse( + changePersonaId.toString(), + "10", + "GOOSE", + ) ) ) @@ -90,7 +95,11 @@ internal class PersonaDeletedSagaHandlerTest( name = "member", totalContributions = "1", personas = listOf( - RenderApi.UserResponse.PersonaResponse(changePersonaId.toString(), "10") + RenderApi.UserResponse.PersonaResponse( + changePersonaId.toString(), + "10", + "GOOSE", + ) ) ) diff --git a/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt b/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt index 66b82a9..2814dcc 100644 --- a/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt +++ b/src/test/kotlin/org/gitanimals/guild/supports/MockApiConfiguration.kt @@ -41,11 +41,13 @@ class MockApiConfiguration { personas = listOf( RenderApi.UserResponse.PersonaResponse( id = "3", - level = "99" + level = "99", + type = "GOOSE", ), RenderApi.UserResponse.PersonaResponse( id = "4", - level = "98" + level = "98", + type = "GOOSE", ), ) ) From bcdaca0e54f36bfc46fafdc0245c31ecb97b354a Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 00:56:38 +0900 Subject: [PATCH 20/24] =?UTF-8?q?refactor:=20=EA=B8=B8=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=EA=B0=80=20personaType=EC=9D=84=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EC=9D=91=EB=8B=B5=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/controller/response/GuildResponse.kt | 6 ++++++ src/main/kotlin/org/gitanimals/guild/domain/Guild.kt | 11 +++++++++++ .../kotlin/org/gitanimals/guild/domain/WaitMember.kt | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt index a842880..c03a8c9 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildResponse.kt @@ -26,6 +26,7 @@ data class GuildResponse( val name: String, val contributions: String, val personaId: String, + val personaType: String, ) data class Member( @@ -34,6 +35,7 @@ data class GuildResponse( val name: String, val contributions: String, val personaId: String, + val personaType: String, ) data class WaitMember( @@ -42,6 +44,7 @@ data class GuildResponse( val name: String, val contributions: String, val personaId: String, + val personaType: String, ) companion object { @@ -57,6 +60,7 @@ data class GuildResponse( name = guild.getLeaderName(), contributions = guild.getContributions().toString(), personaId = guild.getLeaderPersonaId().toString(), + personaType = guild.getLeaderPersonaType(), ), farmType = guild.getGuildFarmType().toString(), totalContributions = guild.getTotalContributions().toString(), @@ -67,6 +71,7 @@ data class GuildResponse( name = it.name, contributions = it.getContributions().toString(), personaId = it.personaId.toString(), + personaType = it.personaType, ) }, waitMembers = guild.getWaitMembers().map { @@ -76,6 +81,7 @@ data class GuildResponse( name = it.name, contributions = it.getContributions().toString(), personaId = it.personaId.toString(), + personaType = it.personaType, ) }, createdAt = guild.createdAt, diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 1e65a1f..598af6d 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -166,12 +166,23 @@ class Guild( it.personaId = changePersonaId it.personaType = changePersonaType } + + waitMembers.firstOrNull { + it.userId == userId && it.personaId == deletedPersonaId + }?.let { + it.personaId = changePersonaId + it.personaType = changePersonaType + } } fun getLeaderPersonaId(): Long { return leader.personaId } + fun getLeaderPersonaType(): String { + return leader.personaType + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt index 77ad036..a50ee1e 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/WaitMember.kt @@ -25,7 +25,7 @@ class WaitMember( val name: String, @Column(name = "persona_id", nullable = false) - val personaId: Long, + var personaId: Long, @Column(name = "persona_type", nullable = false) var personaType: String, From 6c085597056d5f9b3c71cbca497c8c5ff0fc71be Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 21:13:35 +0900 Subject: [PATCH 21/24] =?UTF-8?q?feat:=20=EC=84=A0=ED=83=9D=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=EA=B8=B8=EB=93=9C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=A1=B0=ED=9A=8C=20api=EB=A5=BC=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/controller/GuildController.kt | 10 +++++++ .../controller/response/GuildIconsResponse.kt | 5 ++++ .../org/gitanimals/guild/domain/Guild.kt | 1 + .../org/gitanimals/guild/domain/GuildIcons.kt | 29 +++++++++++++++++++ .../guild/app/CreateGuildFacadeTest.kt | 3 +- .../guild/domain/GuildServiceTest.kt | 2 +- 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/controller/response/GuildIconsResponse.kt create mode 100644 src/main/kotlin/org/gitanimals/guild/domain/GuildIcons.kt diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 71a2105..97320c3 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -3,9 +3,11 @@ package org.gitanimals.guild.controller import org.gitanimals.guild.app.* import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.controller.request.JoinGuildRequest +import org.gitanimals.guild.controller.response.GuildIconsResponse import org.gitanimals.guild.controller.response.GuildPagingResponse import org.gitanimals.guild.controller.response.GuildResponse import org.gitanimals.guild.controller.response.GuildsResponse +import org.gitanimals.guild.domain.GuildIcons import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.domain.SearchFilter import org.gitanimals.guild.domain.request.ChangeGuildRequest @@ -106,4 +108,12 @@ class GuildController( return GuildPagingResponse.from(guilds) } + + @GetMapping("/guilds/icons") + @ResponseStatus(HttpStatus.OK) + fun findAllGuildIcons(): GuildIconsResponse { + return GuildIconsResponse( + GuildIcons.entries.map { it.getImagePath() }.toList() + ) + } } diff --git a/src/main/kotlin/org/gitanimals/guild/controller/response/GuildIconsResponse.kt b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildIconsResponse.kt new file mode 100644 index 0000000..92f7647 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/controller/response/GuildIconsResponse.kt @@ -0,0 +1,5 @@ +package org.gitanimals.guild.controller.response + +data class GuildIconsResponse( + val icons: List, +) diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 598af6d..e19a7ec 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -194,6 +194,7 @@ class Guild( farmType: GuildFarmType, autoJoin: Boolean, ): Guild { + GuildIcons.requireExistImagePath(guildIcon) return Guild( id = IdGenerator.generate(), diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildIcons.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildIcons.kt new file mode 100644 index 0000000..bbbf75a --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildIcons.kt @@ -0,0 +1,29 @@ +package org.gitanimals.guild.domain + +enum class GuildIcons( + private val imageName: String, +) { + CAT("GUILD-1"), + CHICK("GUILD-2"), + FLAMINGO("GUILD-3"), + RABBIT("GUILD-4"), + DESSERT_FOX("GUILD-5"), + GHOST("GUILD-6"), + HAMSTER("GUILD-7"), + SLIME("GUILD-8"), + PIG("GUILD-9"), + PENGUIN("GUILD-10"), + ; + + fun getImagePath() = "$IMAGE_PATH_PREFIX$imageName" + + companion object { + private const val IMAGE_PATH_PREFIX = "https://static.gitanimals.org/guilds/icons/" + + fun requireExistImagePath(imagePath: String) { + require(entries.any { it.getImagePath() == imagePath }) { + "Cannot find matched image by imagePath \"$imagePath\"" + } + } + } +} diff --git a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt index abf442f..9f81c5a 100644 --- a/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/app/CreateGuildFacadeTest.kt @@ -9,6 +9,7 @@ import io.kotest.core.spec.style.DescribeSpec import io.mockk.every import org.gitanimals.guild.app.request.CreateGuildRequest import org.gitanimals.guild.domain.GuildFarmType +import org.gitanimals.guild.domain.GuildIcons import org.gitanimals.guild.domain.GuildRepository import org.gitanimals.guild.domain.GuildService import org.gitanimals.guild.supports.RedisContainer @@ -108,7 +109,7 @@ internal class CreateGuildFacadeTest( private val createGuildRequest = CreateGuildRequest( title = "Gitanimals", body = "We are gitanimals", - guildIcon = "gitanimals.org", + guildIcon = GuildIcons.CAT.getImagePath(), autoJoin = true, farmType = GuildFarmType.DUMMY, personaId = "3", diff --git a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt index afc152d..ccedf29 100644 --- a/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt +++ b/src/test/kotlin/org/gitanimals/guild/domain/GuildServiceTest.kt @@ -29,7 +29,7 @@ internal class GuildServiceTest( describe("createGuild 메소드는") { context("guild 정보와 leader 정보를 입력받으면") { - val guildIcon = "guildIcon" + val guildIcon = GuildIcons.CAT.getImagePath() val title = "guildTitle" val body = "guildBody" val farmType = GuildFarmType.DUMMY From 9c7b5dda89cc1f5d410ff2d5cab788e3fb84399f Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 21:23:11 +0900 Subject: [PATCH 22/24] =?UTF-8?q?feat:=20=EA=B8=B8=EB=93=9C=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B1=B0=EC=A0=88=20api=EB=A5=BC=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/app/AcceptJoinGuildFacade.kt | 6 +++++- .../gitanimals/guild/app/DenyJoinGuildFacade.kt | 17 +++++++++++++++++ .../kotlin/org/gitanimals/guild/domain/Guild.kt | 4 ++++ .../org/gitanimals/guild/domain/GuildService.kt | 8 ++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/DenyJoinGuildFacade.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt index 9d4af32..494480e 100644 --- a/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt +++ b/src/main/kotlin/org/gitanimals/guild/app/AcceptJoinGuildFacade.kt @@ -12,6 +12,10 @@ class AcceptJoinGuildFacade( fun acceptJoin(token: String, guildId: Long, acceptUserId: Long) { val user = identityApi.getUserByToken(token) - guildService.acceptJoin(user.id.toLong(), guildId = guildId, acceptUserId = acceptUserId) + guildService.acceptJoin( + acceptorId = user.id.toLong(), + guildId = guildId, + acceptUserId = acceptUserId, + ) } } diff --git a/src/main/kotlin/org/gitanimals/guild/app/DenyJoinGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/DenyJoinGuildFacade.kt new file mode 100644 index 0000000..0067b5a --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/DenyJoinGuildFacade.kt @@ -0,0 +1,17 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Service + +@Service +class DenyJoinGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun denyJoin(token: String, guildId: Long, denyUserId: Long) { + val user = identityApi.getUserByToken(token) + + guildService.denyJoin(denierId = user.id.toLong(), guildId = guildId, denyUserId = denyUserId) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index e19a7ec..1a76297 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -110,6 +110,10 @@ class Guild( members.add(acceptUser.toMember()) } + fun deny(denyUserId: Long) { + waitMembers.removeIf { it.userId == denyUserId } + } + fun kickMember(kickUserId: Long) { members.removeIf { it.userId == kickUserId } } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index 116c8b8..f968035 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -70,6 +70,14 @@ class GuildService( guild.accept(acceptUserId) } + @Transactional + fun denyJoin(denierId: Long, guildId: Long, denyUserId: Long) { + val guild = guildRepository.findGuildByIdAndLeaderId(guildId, denierId) + ?: throw IllegalArgumentException("Cannot deny join cause your not a leader.") + + guild.deny(denyUserId) + } + @Transactional fun kickMember(kickerId: Long, guildId: Long, kickUserId: Long) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, kickerId) From 673caa81dc196b10db0ca7b74ac5815012238d8b Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 21:32:17 +0900 Subject: [PATCH 23/24] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=ED=8E=AB=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20api=EB=A5=BC=20=EA=B0=9C=EB=B0=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../guild/app/ChangeMainPersonaFacade.kt | 27 +++++++++++++++++++ .../guild/controller/GuildController.kt | 12 +++++++++ .../org/gitanimals/guild/domain/Guild.kt | 15 +++++++++++ .../gitanimals/guild/domain/GuildService.kt | 7 +++++ 4 files changed, 61 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/ChangeMainPersonaFacade.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/ChangeMainPersonaFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/ChangeMainPersonaFacade.kt new file mode 100644 index 0000000..0cceeb6 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/ChangeMainPersonaFacade.kt @@ -0,0 +1,27 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Component + +@Component +class ChangeMainPersonaFacade( + private val renderApi: RenderApi, + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun changeMainPersona(token: String, guildId: Long, personaId: Long) { + val user = identityApi.getUserByToken(token) + val personas = renderApi.getUserByName(user.username).personas + + val changedPersona = personas.firstOrNull { it.id.toLong() == personaId } + ?: throw IllegalArgumentException("Cannot change persona to \"$personaId\" from user \"${user.username}\"") + + guildService.changeMainPersona( + guildId = guildId, + userId = user.id.toLong(), + personaId = changedPersona.id.toLong(), + personaType = changedPersona.type, + ) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 97320c3..444fb4d 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -25,6 +25,7 @@ class GuildController( private val changeGuildFacade: ChangeGuildFacade, private val joinedGuildFacade: GetJoinedGuildFacade, private val searchGuildFacade: SearchGuildFacade, + private val changeMainPersonaFacade: ChangeMainPersonaFacade, ) { @ResponseStatus(HttpStatus.OK) @@ -116,4 +117,15 @@ class GuildController( GuildIcons.entries.map { it.getImagePath() }.toList() ) } + + @PostMapping("/guilds/{guildId}/personas") + fun changeMainPersona( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + @RequestParam("persona-id") personaId: Long, + ) = changeMainPersonaFacade.changeMainPersona( + token = token, + guildId = guildId, + personaId = personaId, + ) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index 1a76297..ecd18d8 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -187,6 +187,21 @@ class Guild( return leader.personaType } + fun changeMainPersona(userId: Long, personaId: Long, personaType: String) { + if (leader.userId == userId) { + leader.personaId = personaId + leader.personaType = personaType + return + } + + members.first { + it.userId == userId + }.run { + this.personaId = personaId + this.personaType = personaType + } + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index f968035..d0476ec 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -94,6 +94,13 @@ class GuildService( guild.change(request) } + @Transactional + fun changeMainPersona(guildId: Long, userId: Long, personaId: Long, personaType: String) { + val guild = getGuildById(guildId) + + guild.changeMainPersona(userId, personaId, personaType) + } + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { val guild = guildRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") From debf5b1026a3b999a10678537d42555eee869c29 Mon Sep 17 00:00:00 2001 From: devxb Date: Mon, 23 Dec 2024 21:39:59 +0900 Subject: [PATCH 24/24] =?UTF-8?q?feat:=20leave=20guild=20api=EB=A5=BC=20?= =?UTF-8?q?=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 --- .../gitanimals/guild/app/LeaveGuildFacade.kt | 17 +++++++++++++++++ .../guild/controller/GuildController.kt | 7 +++++++ .../org/gitanimals/guild/domain/Guild.kt | 8 ++++++++ .../gitanimals/guild/domain/GuildService.kt | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 src/main/kotlin/org/gitanimals/guild/app/LeaveGuildFacade.kt diff --git a/src/main/kotlin/org/gitanimals/guild/app/LeaveGuildFacade.kt b/src/main/kotlin/org/gitanimals/guild/app/LeaveGuildFacade.kt new file mode 100644 index 0000000..cc0a2ce --- /dev/null +++ b/src/main/kotlin/org/gitanimals/guild/app/LeaveGuildFacade.kt @@ -0,0 +1,17 @@ +package org.gitanimals.guild.app + +import org.gitanimals.guild.domain.GuildService +import org.springframework.stereotype.Component + +@Component +class LeaveGuildFacade( + private val identityApi: IdentityApi, + private val guildService: GuildService, +) { + + fun leave(token: String, guildId: Long) { + val user = identityApi.getUserByToken(token) + + guildService.leave(guildId, user.id.toLong()) + } +} diff --git a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt index 444fb4d..383e3c5 100644 --- a/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt +++ b/src/main/kotlin/org/gitanimals/guild/controller/GuildController.kt @@ -26,6 +26,7 @@ class GuildController( private val joinedGuildFacade: GetJoinedGuildFacade, private val searchGuildFacade: SearchGuildFacade, private val changeMainPersonaFacade: ChangeMainPersonaFacade, + private val leaveGuildFacade: LeaveGuildFacade, ) { @ResponseStatus(HttpStatus.OK) @@ -128,4 +129,10 @@ class GuildController( guildId = guildId, personaId = personaId, ) + + @DeleteMapping("/guilds/{guildId}/leave") + fun leaveGuild( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @PathVariable("guildId") guildId: Long, + ) = leaveGuildFacade.leave(token, guildId) } diff --git a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt index ecd18d8..bc907a1 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/Guild.kt @@ -202,6 +202,14 @@ class Guild( } } + fun leave(userId: Long) { + require(userId != leader.userId) { + "Leader cannot leave guild guildId: \"$id\", userId: \"$userId\"" + } + + members.removeIf { it.userId == userId } + } + companion object { fun create( diff --git a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt index d0476ec..66aae0e 100644 --- a/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt +++ b/src/main/kotlin/org/gitanimals/guild/domain/GuildService.kt @@ -6,6 +6,8 @@ import org.hibernate.Hibernate import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull +import org.springframework.orm.ObjectOptimisticLockingFailureException +import org.springframework.retry.annotation.Retryable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -43,6 +45,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun joinGuild( guildId: Long, memberUserId: Long, @@ -63,6 +66,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun acceptJoin(acceptorId: Long, guildId: Long, acceptUserId: Long) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, acceptorId) ?: throw IllegalArgumentException("Cannot accept join cause your not a leader.") @@ -71,6 +75,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun denyJoin(denierId: Long, guildId: Long, denyUserId: Long) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, denierId) ?: throw IllegalArgumentException("Cannot deny join cause your not a leader.") @@ -79,6 +84,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun kickMember(kickerId: Long, guildId: Long, kickUserId: Long) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, kickerId) ?: throw IllegalArgumentException("Cannot kick member cause your not a leader.") @@ -87,6 +93,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun changeGuild(changeRequesterId: Long, guildId: Long, request: ChangeGuildRequest) { val guild = guildRepository.findGuildByIdAndLeaderId(guildId, changeRequesterId) ?: throw IllegalArgumentException("Cannot kick member cause your not a leader.") @@ -95,12 +102,21 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun changeMainPersona(guildId: Long, userId: Long, personaId: Long, personaType: String) { val guild = getGuildById(guildId) guild.changeMainPersona(userId, personaId, personaType) } + @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) + fun leave(guildId: Long, userId: Long) { + val guild = getGuildById(guildId) + + guild.leave(userId) + } + fun getGuildById(id: Long, vararg lazyLoading: (Guild) -> Unit): Guild { val guild = guildRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Cannot fint guild by id \"$id\"") @@ -110,6 +126,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun updateContribution(username: String, contributions: Long) { val guilds = guildRepository.findAllGuildByUsernameWithMembers(username) @@ -138,6 +155,7 @@ class GuildService( } @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) fun deletePersonaSync( userId: Long, deletedPersonaId: Long,