Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Member 도메인 설계 #3

Merged
merged 10 commits into from
May 3, 2024
10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,24 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0")
implementation("com.github.f4b6a3:ulid-creator:5.2.3")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")

val koTestVersion = "5.8.1"
testImplementation("io.kotest:kotest-runner-junit5-jvm:$koTestVersion")
testImplementation("io.kotest:kotest-assertions-core:$koTestVersion")
testImplementation("io.kotest:kotest-property:$koTestVersion")

testImplementation("io.mockk:mockk:1.13.10")

}

tasks.withType<KotlinCompile> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import org.springframework.context.annotation.FilterType

@SpringBootApplication
@ComponentScan(
includeFilters = [ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = [UseCase::class]
)]
includeFilters = [ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = [UseCase::class]
)]
)
class DepromeetMakersBeApplication

fun main(args: Array<String>) {
runApplication<DepromeetMakersBeApplication>(*args)
runApplication<DepromeetMakersBeApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.depromeet.makers.domain.exception

open class DefaultException(
errorCode: ErrorCode,
) : DomainException(errorCode)

class UnknownErrorException : DefaultException(ErrorCode.UNKNOWN_SERVER_ERROR)
class UnauthorizedException : DefaultException(ErrorCode.UNAUTHORIZED)
class InvalidInputException : DefaultException(ErrorCode.INVALID_INPUT)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.depromeet.makers.domain.exception

open class DomainException(
val errorCode: ErrorCode,
val data: Any? = null,
) : RuntimeException(errorCode.message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.depromeet.makers.domain.exception

enum class ErrorCode(
val code: String,
val message: String,
) {
/**
* 기본 오류
* @see com.depromeet.makers.domain.exception.DefaultException
*/
UNKNOWN_SERVER_ERROR("DE0001", "알 수 없는 오류가 발생했습니다"),
UNAUTHORIZED("DE0002", "인가되지 않은 접근입니다"),
INVALID_INPUT("DE0003", "입력값(바디 혹은 파라미터)가 누락되었습니다"),

/**
* 사용자 관련 오류
* @see com.depromeet.makers.domain.exception.MemberException
*/
MEMBER_ALREADY_EXISTS("ME0001", "해당 이메일의 사용자가 이미 존재합니다"),
PASSCORD_ALREADY_SET("ME0002", "이미 비밀번호가 지정되었습니다. 관리자에게 문의하세요")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.depromeet.makers.domain.exception

open class MemberException(
errorCode: ErrorCode,
) : DomainException(errorCode)

class MemberAlreadyExistsException : MemberException(ErrorCode.MEMBER_ALREADY_EXISTS)
class PassCordAlreadySetException : MemberException(ErrorCode.PASSCORD_ALREADY_SET)
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ package com.depromeet.makers.domain.gateway
import com.depromeet.makers.domain.model.Member

interface MemberGateway {
fun findByEmail(email: String): Member?
fun getById(memberId: String): Member
fun save(member: Member): Member
}
27 changes: 26 additions & 1 deletion src/main/kotlin/com/depromeet/makers/domain/model/Member.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
package com.depromeet.makers.domain.model

import com.depromeet.makers.util.generateULID

data class Member(
val memberId: String,
val name: String,
)
val email: String,
val passCord: String?,
val generations: Set<MemberGeneration>,
) {
fun hasPassCord() = passCord != null

fun initializePassCord(passCord: String) = this.copy(
passCord = passCord
)

companion object {
fun newMember(
name: String,
email: String,
initialGeneration: MemberGeneration,
) = Member(
memberId = generateULID(),
name = name,
email = email,
passCord = null,
generations = setOf(initialGeneration)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.depromeet.makers.domain.model

data class MemberGeneration(
val generationId: Int,
val groupId: Int?,
val role: MemberRole,
val position: MemberPosition,
) {
fun hasGroup() = groupId != null
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.depromeet.makers.domain.model

enum class MemberPosition {
BACKEND, IOS, AOS, WEB, DESIGN, UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.depromeet.makers.domain.model

enum class MemberRole {
ORGANIZER, MEMBER
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
package com.depromeet.makers.domain.usecase

import com.depromeet.makers.domain.exception.MemberAlreadyExistsException
import com.depromeet.makers.domain.gateway.MemberGateway
import com.depromeet.makers.domain.model.Member
import com.depromeet.makers.domain.model.MemberGeneration
import com.depromeet.makers.domain.model.MemberPosition
import com.depromeet.makers.domain.model.MemberRole

class CreateNewMember(
private val memberGateway: MemberGateway,
): UseCase<CreateNewMember.CreateNewMemberInput, Member> {
) : UseCase<CreateNewMember.CreateNewMemberInput, Member> {
data class CreateNewMemberInput(
val name: String
val name: String,
val email: String,
val generationId: Int,
val groupId: Int?,
val role: MemberRole,
val position: MemberPosition,
)

override fun execute(input: CreateNewMemberInput): Member {
val member = Member(
memberId = "",
name = input.name,
val previousMember = memberGateway.findByEmail(input.email)
val newGeneration = MemberGeneration(
generationId = input.generationId,
role = input.role,
position = input.position,
groupId = input.groupId,
)
return memberGateway.save(member)

val newMember = if (previousMember != null) {
// 이미 있으면 기존 멤버에 기수 데이터 추가
if (previousMember.generations.any { it.generationId == newGeneration.generationId }) {
throw MemberAlreadyExistsException()
}

val newGenerations = previousMember.generations + newGeneration
previousMember.copy(
generations = newGenerations
)
} else {
// 없으면 새 멤버
Member.newMember(
name = input.name,
email = input.email,
initialGeneration = newGeneration,
)
}
return memberGateway.save(newMember)
CChuYong marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.depromeet.makers.domain.usecase

import com.depromeet.makers.domain.exception.PassCordAlreadySetException
import com.depromeet.makers.domain.gateway.MemberGateway
import com.depromeet.makers.domain.model.Member

class UpdateDefaultMemberPassCord(
private val memberGateway: MemberGateway,
private val updateMemberPassCord: UpdateMemberPassCord,
) : UseCase<UpdateDefaultMemberPassCord.UpdateDefaultMemberPassCordInput, Member> {
data class UpdateDefaultMemberPassCordInput(
val memberId: String,
val passCord: String,
)

override fun execute(input: UpdateDefaultMemberPassCordInput): Member {
val member = memberGateway.getById(input.memberId)
if (isPassCordInitialized(member)) throw PassCordAlreadySetException()

return updateMemberPassCord.execute(
UpdateMemberPassCord.UpdateMemberPassCordInput(
memberId = input.memberId,
passCord = input.passCord,
)
)
}

private fun isPassCordInitialized(member: Member) = member.hasPassCord()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.depromeet.makers.domain.usecase

import com.depromeet.makers.domain.gateway.MemberGateway
import com.depromeet.makers.domain.model.Member

class UpdateMemberPassCord(
private val memberGateway: MemberGateway,
) : UseCase<UpdateMemberPassCord.UpdateMemberPassCordInput, Member> {
data class UpdateMemberPassCordInput(
val memberId: String,
val passCord: String,
)

override fun execute(input: UpdateMemberPassCordInput): Member {
val member = memberGateway.getById(input.memberId)
return memberGateway.save(
member.initializePassCord(passCord = input.passCord)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
package com.depromeet.makers.infrastructure.db.entity

import com.depromeet.makers.domain.model.Member
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.*

@Entity(name = "member")
data class MemberEntity(
class MemberEntity private constructor(
@Id
@Column(name = "id")
@Column(name = "id", length = 26, columnDefinition = "CHAR(26)", nullable = false)
val id: String,

@Column(name = "name")
@Column(name = "name", nullable = false)
val name: String,

@Column(name = "email", nullable = false, unique = true)
val email: String,

@Column(name = "passcord", nullable = true)
var passCord: String?,

@OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
@JoinColumn(name = "member_id")
var generations: Set<MemberGenerationEntity>,
) {

fun toDomain() = Member(
memberId = id,
name = name,
email = email,
passCord = passCord,
generations = generations
.map(MemberGenerationEntity::toDomain)
.toSet()
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as MemberEntity

return id == other.id
}

override fun hashCode(): Int {
return id.hashCode()
}

companion object {
fun fromDomain(member: Member) = with(member) {
MemberEntity(
id = memberId,
name = name,
email = email,
passCord = passCord,
generations = generations
.map { MemberGenerationEntity.fromDomain(memberId, it) }
.toSet()
)
}
}
Expand Down
Loading