Skip to content
This repository was archived by the owner on Nov 21, 2023. It is now read-only.

Commit 4456124

Browse files
committed
[#28] 가입기능 구현
1 parent 90dead8 commit 4456124

File tree

17 files changed

+270
-22
lines changed

17 files changed

+270
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.sns.commons.utils
2+
3+
inline fun Boolean?.ifTrue(block: Boolean.() -> Unit): Boolean? {
4+
if (this == true) {
5+
block()
6+
}
7+
return this
8+
}
9+
10+
inline fun Boolean?.ifFalse(block: Boolean.() -> Unit): Boolean? {
11+
if (this == true) {
12+
block()
13+
}
14+
return this
15+
}

user-api/src/main/kotlin/com/sns/user/component/authcode/application/AuthCodeCommand.kt

+2-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Service
1313
class AuthCodeCommand(
1414
val authCodeRepository: AuthCodeRepository,
1515
val mailService: MailService,
16-
val userRepository: DefaultUserRepository
16+
val userRepository: DefaultUserRepository,
1717
) {
1818
fun create(userId: String): AuthCode {
1919
val user = userRepository.findByIdOrNull(userId) ?: throw NoAuthorityException()
@@ -27,9 +27,6 @@ class AuthCodeCommand(
2727

2828
fun verify(userId: String, purpose: Purpose, code: String): Boolean {
2929
val authCode: AuthCode? = authCodeRepository.findByUserIdAndPurpose(userId, purpose)
30-
return authCode?.isCorrect(userId, code, purpose)
31-
.takeIf { it == true }.apply {
32-
// TOOD update STATUS userRepository.save()
33-
} ?: false
30+
return authCode?.isCorrect(userId, code, purpose) ?: false
3431
}
3532
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.sns.user.component.user.application
2+
3+
import com.sns.user.component.user.domains.User
4+
import com.sns.user.component.user.repositories.UserRepository
5+
import com.sns.user.core.exceptions.NoAuthorityException
6+
import org.springframework.context.ApplicationEventPublisher
7+
import org.springframework.data.repository.findByIdOrNull
8+
import org.springframework.security.crypto.password.PasswordEncoder
9+
import org.springframework.stereotype.Service
10+
11+
@Service
12+
class UserCommandService(
13+
private val userRepository: UserRepository,
14+
private val passwordEncoder: PasswordEncoder,
15+
private val eventPublisher: ApplicationEventPublisher
16+
) {
17+
18+
fun create(name: String, password: String, email: String): User {
19+
val user = User.create(email, passwordEncoder.encode(password), name, email) {
20+
eventPublisher.publishEvent(it)
21+
}
22+
userRepository.save(user)
23+
return user
24+
}
25+
26+
fun activate(userId: String) {
27+
val user = userRepository.findByIdOrNull(userId) ?: throw NoAuthorityException()
28+
29+
user.activate() {
30+
eventPublisher.publishEvent(it)
31+
}
32+
userRepository.save(user)
33+
}
34+
}

user-api/src/main/kotlin/com/sns/user/component/user/application/UserQueryService.kt

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ class UserQueryService(
1010
private val userRepository: UserRepository
1111
) {
1212
fun getById(id: String): User? = userRepository.findByIdOrNull(id)
13+
14+
fun getByEmail(email: String): User? = userRepository.findByInfoEmailAddressOrNull(email)
1315
}

user-api/src/main/kotlin/com/sns/user/component/user/domains/User.kt

+45-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.sns.user.component.user.domains
22

3+
import com.sns.commons.DomainEvent
4+
import com.sns.user.component.user.events.UserActivatedEvent
5+
import com.sns.user.component.user.events.UserCreatedEvent
6+
import java.sql.ResultSet
37
import java.time.Instant
48
import javax.validation.constraints.Max
59
import javax.validation.constraints.NotBlank
@@ -8,6 +12,7 @@ import org.springframework.data.annotation.Id
812
import org.springframework.data.annotation.LastModifiedDate
913
import org.springframework.data.annotation.Transient
1014
import org.springframework.data.domain.Persistable
15+
import org.springframework.jdbc.core.RowMapper
1116

1217
data class User(
1318
@Id
@@ -32,27 +37,64 @@ data class User(
3237

3338
@LastModifiedDate
3439
var updatedAt: Instant = Instant.MIN,
40+
41+
@NotBlank
42+
var status: Status = Status.ACTIVATED
3543
) : Persistable<String> {
3644
@Transient
3745
private var new: Boolean = false
3846

3947
override fun getId() = this.id
4048
override fun isNew() = new
4149

50+
fun activate(publish: (DomainEvent) -> Unit = { _ -> }) {
51+
status = Status.ACTIVATED
52+
publish(UserActivatedEvent(this))
53+
}
54+
4255
companion object {
56+
val MAPPER: RowMapper<User> = UserRowMapper()
57+
4358
fun create(
4459
id: String,
4560
password: String,
4661
name: String,
47-
infoEmailAddress: String? = null
62+
infoEmailAddress: String? = null,
63+
publish: (DomainEvent) -> Unit = { _ -> }
4864
): User {
4965
// TODO validation
50-
return User(
66+
val user = User(
5167
id = id,
52-
password = password, // TODO encrypt
68+
password = password,
5369
name = name,
5470
infoEmailAddress = infoEmailAddress ?: id,
71+
status = Status.ON_SIGN_UP,
5572
).apply { new = true }
73+
74+
publish(UserCreatedEvent(user))
75+
76+
return user
5677
}
5778
}
5879
}
80+
81+
// purpose enum 매핑이 안되서 수동으로 작성함. 확인필요.
82+
class UserRowMapper : RowMapper<User> {
83+
override fun mapRow(rs: ResultSet, rowNum: Int): User? {
84+
return User(
85+
id = rs.getString("id"),
86+
password = rs.getString("password"),
87+
name = rs.getString("name"),
88+
infoEmailAddress = rs.getString("info_email_address"),
89+
status = Status.valueOf(rs.getString("status")),
90+
createdAt = Instant.ofEpochMilli(rs.getTimestamp("created_at").time),
91+
updatedAt = Instant.ofEpochMilli(rs.getTimestamp("updated_at").time),
92+
)
93+
}
94+
}
95+
96+
enum class Status {
97+
ON_SIGN_UP,
98+
ACTIVATED,
99+
// 비활 등등?
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.sns.user.component.user.events
2+
3+
import com.sns.commons.DomainEvent
4+
import com.sns.user.component.user.domains.User
5+
import com.sns.user.core.config.IntegrationConfig
6+
7+
class UserActivatedEvent(val user: User) : DomainEvent {
8+
override val eventId: String
9+
get() = "$channel-$user.id-${System.currentTimeMillis()}"
10+
11+
override val channel = IntegrationConfig.Channels.USER_STATUS
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.sns.user.component.user.events
2+
3+
import com.sns.commons.DomainEvent
4+
import com.sns.user.component.user.domains.User
5+
import com.sns.user.core.config.IntegrationConfig
6+
7+
class UserCreatedEvent(val user: User) : DomainEvent {
8+
override val eventId: String
9+
get() = "$channel-$user.id-${System.currentTimeMillis()}"
10+
11+
override val channel = IntegrationConfig.Channels.USER_STATUS
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.sns.user.component.user.listeners
2+
3+
import com.sns.commons.annotation.CustomEventListener
4+
import com.sns.user.component.authcode.application.AuthCodeCommand
5+
import com.sns.user.component.user.events.UserActivatedEvent
6+
import com.sns.user.component.user.events.UserCreatedEvent
7+
8+
@CustomEventListener
9+
class UserStatusListener(val authCodeCommand: AuthCodeCommand) {
10+
// 인증 전, 기초 가입만 마친 상태
11+
fun onCreated(createdEvent: UserCreatedEvent) {
12+
val user = createdEvent.user
13+
authCodeCommand.create(user.id)
14+
}
15+
16+
fun onActivated(activatedEvent: UserActivatedEvent) {
17+
// TODO 타임라인생성, 프로필생성,,
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
package com.sns.user.component.user.repositories
22

3+
import com.sns.user.component.user.domains.User
4+
import org.springframework.jdbc.core.JdbcTemplate
35
import org.springframework.stereotype.Repository
46

57
@Repository
68
class DefaultUserRepository(
7-
userCrudRepository: UserCrudRepository
9+
userCrudRepository: UserCrudRepository,
10+
private val jdbcTemplate: JdbcTemplate
811
) : UserRepository,
9-
UserCrudRepository by userCrudRepository
12+
UserCrudRepository by userCrudRepository {
13+
14+
override fun findByInfoEmailAddressOrNull(email: String): User? = jdbcTemplate.queryForObject(
15+
"""
16+
SELECT `id`,`password`,`name`,info_email_address,created_at,updated_at,`status`
17+
FROM `user`
18+
WHERE info_email_address = ?
19+
LIMIT 1
20+
""".trimIndent(),
21+
User.MAPPER, email,
22+
)
23+
}

user-api/src/main/kotlin/com/sns/user/component/user/repositories/UserRepository.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ import org.springframework.data.repository.CrudRepository
55
import org.springframework.data.repository.NoRepositoryBean
66

77
@NoRepositoryBean
8-
interface UserRepository : CrudRepository<User, String>
8+
interface UserRepository : CrudRepository<User, String> {
9+
fun findByInfoEmailAddressOrNull(email: String): User?
10+
}

user-api/src/main/kotlin/com/sns/user/core/config/IntegrationConfig.kt

+15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.sns.user.core.config
33
import com.sns.commons.config.IntegrationEventBaseConfig
44
import com.sns.user.component.test.dtos.LaughingEvent
55
import com.sns.user.component.test.listeners.EmotionListener
6+
import com.sns.user.component.user.events.UserActivatedEvent
7+
import com.sns.user.component.user.events.UserCreatedEvent
8+
import com.sns.user.component.user.listeners.UserStatusListener
69
import org.springframework.context.annotation.Bean
710
import org.springframework.context.annotation.Configuration
811
import org.springframework.context.annotation.Import
@@ -20,7 +23,19 @@ class IntegrationConfig {
2023
}
2124
}
2225

26+
@Bean
27+
fun userStatusFlow(userStatusListener: UserStatusListener) = integrationFlow {
28+
channel { publishSubscribe(Channels.USER_STATUS) }
29+
handle<UserCreatedEvent> { event, _ ->
30+
userStatusListener.onCreated(event)
31+
}
32+
handle<UserActivatedEvent> { event, _ ->
33+
userStatusListener.onActivated(event)
34+
}
35+
}
36+
2337
object Channels {
2438
const val EMOTION = "EMOTION_CHANNEL"
39+
const val USER_STATUS = "USER_STATUS_CHANNEL"
2540
}
2641
}

user-api/src/main/kotlin/com/sns/user/endpoints/user/SignUpController.kt

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.sns.user.endpoints.user
22

3+
import com.sns.commons.utils.ifTrue
34
import com.sns.user.component.authcode.application.AuthCodeCommand
45
import com.sns.user.component.authcode.domain.Purpose
6+
import com.sns.user.component.user.application.UserCommandService
7+
import com.sns.user.component.user.application.UserQueryService
58
import com.sns.user.core.config.SwaggerTag
69
import com.sns.user.endpoints.user.requests.SignUpRequest
710
import io.swagger.v3.oas.annotations.media.Content
@@ -25,13 +28,17 @@ import org.springframework.web.bind.annotation.RestController
2528
@RestController
2629
@Tag(name = SwaggerTag.SIGN_UP)
2730
@RequestMapping("/api")
28-
class SignUpController(val authCodeCommand: AuthCodeCommand) {
31+
class SignUpController(
32+
val authCodeCommand: AuthCodeCommand,
33+
val userQueryService: UserQueryService,
34+
val userCommandService: UserCommandService
35+
) {
2936

3037
@ApiResponse(description = "회원 가입", responseCode = "202")
3138
@ResponseStatus(HttpStatus.CREATED)
3239
@PostMapping("/v1/sign-up")
3340
fun signUp(@RequestBody request: SignUpRequest) {
34-
// TODO 패스워드 유효성 검증
41+
userCommandService.create(request.name, request.password, request.email)
3542
}
3643

3744
@ApiResponse(
@@ -41,15 +48,14 @@ class SignUpController(val authCodeCommand: AuthCodeCommand) {
4148
@ResponseStatus(HttpStatus.OK)
4249
@GetMapping("/v1/sign-up/verifications/emails/{email}")
4350
fun verifyEmail(@Email @PathVariable email: String): ResponseEntity<Boolean> {
44-
// TODO email 중복 검사
45-
return ResponseEntity.ok(false)
51+
return (userQueryService.getByEmail(email) != null)
52+
.let { ResponseEntity.ok(it) }
4653
}
4754

4855
@ApiResponse(description = "가입 인증 코드 재발송", responseCode = "202")
4956
@ResponseStatus(HttpStatus.CREATED)
5057
@PutMapping("/v1/sign-up/verifications/auth-code/ids/{userId}")
5158
fun createAuthenticationCode(@PathVariable userId: String) {
52-
5359
authCodeCommand.create(userId)
5460
}
5561

@@ -60,6 +66,10 @@ class SignUpController(val authCodeCommand: AuthCodeCommand) {
6066
@ResponseStatus(HttpStatus.OK)
6167
@PostMapping("/v1/sign-up/verifications/auth-code/ids/{userId}")
6268
fun verifyAuthenticationCode(@PathVariable userId: String, @RequestBody code: String): ResponseEntity<Boolean> {
63-
return ResponseEntity.ok(authCodeCommand.verify(userId, Purpose.SIGN_UP, code))
69+
return authCodeCommand.verify(userId, Purpose.SIGN_UP, code)
70+
.ifTrue { userCommandService.activate(userId) }
71+
.let {
72+
ResponseEntity.ok(it)
73+
}
6474
}
6575
}

user-api/src/main/kotlin/com/sns/user/endpoints/user/requests/SignUpRequest.kt

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package com.sns.user.endpoints.user.requests
22

33
import javax.validation.constraints.Email
4+
import javax.validation.constraints.Max
45
import javax.validation.constraints.NotEmpty
5-
import org.hibernate.validator.constraints.Length
6+
import javax.validation.constraints.Pattern
7+
import javax.validation.constraints.Size
68

79
data class SignUpRequest(
810
@NotEmpty
9-
@Length(max = 15)
11+
@Max(15)
1012
val name: String,
13+
1114
@NotEmpty
12-
@Length(min = 8, max = 30)
15+
@Size(min = 8, max = 30, message = "비밀번호는 8자 이상 30자 미만이어야 합니다.")
16+
@Pattern(regexp = "(?=.*[A-z])(?=.*[0-9])", message = "비밀번호는 영문자와 숫자가 포함되어야 합니다.")
1317
val password: String,
18+
1419
@NotEmpty
1520
@Email
1621
val email: String,

0 commit comments

Comments
 (0)