Skip to content

Commit

Permalink
[DPMBE-120] 약속 초대 코드를 생성한다 (#204)
Browse files Browse the repository at this point in the history
* feat : InviteCode redis Entity save

* feat : promiseId 의 고유 초대 코드생성

* feat : 약속 생성시, 약속 코드 생성 및 필드 추가

* feat : inviteCode 조회

* refactor : upsert 로직 개선
test 코드 작성

* feat : InviteCode로 약속 참여하기

* feat : 초대코드 관련 에러 코드 Swagger 추가

* feat : InviteCode 길이 validation aop
  • Loading branch information
BlackBean99 authored Jul 16, 2023
1 parent 251afe6 commit d19ff0e
Show file tree
Hide file tree
Showing 23 changed files with 370 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package com.depromeet.whatnow
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.EnableAspectJAutoProxy
import org.springframework.web.filter.ForwardedHeaderFilter

@SpringBootApplication
@EnableAspectJAutoProxy
class WhatnowApiApplication {
companion object {
@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.depromeet.whatnow.api.example.controller
import com.depromeet.whatnow.annotation.ApiErrorCodeExample
import com.depromeet.whatnow.api.config.kakao.KakaoKauthErrorCode
import com.depromeet.whatnow.domains.image.exception.ImageErrorCode
import com.depromeet.whatnow.domains.invitecode.exception.InviteCodeErrorCode
import com.depromeet.whatnow.domains.progresshistory.exception.PromiseHistoryErrorCode
import com.depromeet.whatnow.domains.promise.exception.PromiseErrorCode
import com.depromeet.whatnow.domains.promiseuser.exception.PromiseUserErrorCode
Expand Down Expand Up @@ -57,4 +58,9 @@ class ExampleController() {
@Operation(summary = "약속 참여 관련 에러코드 나열")
@ApiErrorCodeExample(PromiseUserErrorCode::class)
fun promiseUserErrorCode() {}

@GetMapping("/invite-codes")
@Operation(summary = "약속 초대 코드 에러코드 나열")
@ApiErrorCodeExample(InviteCodeErrorCode::class)
fun inviteCodeErrorCode() {}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.depromeet.whatnow.api.promise.controller

import com.depromeet.whatnow.api.promise.annotation.RequiresMainUser
import com.depromeet.whatnow.api.promise.dto.PromiseCreateDto
import com.depromeet.whatnow.api.promise.dto.PromiseDetailDto
import com.depromeet.whatnow.api.promise.dto.PromiseDto
import com.depromeet.whatnow.api.promise.dto.PromiseFindDto
import com.depromeet.whatnow.api.promise.dto.PromiseRequest
import com.depromeet.whatnow.api.promise.usecase.InviteCodeReadUseCase
import com.depromeet.whatnow.api.promise.usecase.PromiseReadUseCase
import com.depromeet.whatnow.api.promise.usecase.PromiseRegisterUseCase
import com.depromeet.whatnow.common.vo.PlaceVo
Expand Down Expand Up @@ -32,6 +34,7 @@ import java.time.YearMonth
class PromiseController(
val promiseRegisterUseCase: PromiseRegisterUseCase,
val promiseReadUseCase: PromiseReadUseCase,
val inviteCodeReadUseCase: InviteCodeReadUseCase,
) {
@Deprecated("나의 약속 전부 조회", replaceWith = ReplaceWith("findPromiseByStatus"))
@Operation(summary = "나의 약속 전부 조회", description = "유저의 약속 전부 조회 (단, 예정된 약속과 지난 약속을 구분해서 조회")
Expand Down Expand Up @@ -68,9 +71,15 @@ class PromiseController(
return promiseReadUseCase.findByPromiseId(promiseId)
}

@Operation(summary = "promiseId 로 약속 초대 코드 조회", description = "promiseId 로 약속 초대 코드 조회하기")
@GetMapping("/promises/{promise-id}/invite-codes")
fun findInviteCodeByPromiseId(@PathVariable(value = "promise-id") promiseId: Long): String {
return inviteCodeReadUseCase.findInviteCodeByPromiseId(promiseId)
}

@Operation(summary = "약속(promise) 생성", description = "약속을 생성합니다.")
@PostMapping("/promises")
fun createPromise(@RequestBody promiseRequest: PromiseRequest): PromiseDto {
fun createPromise(@RequestBody promiseRequest: PromiseRequest): PromiseCreateDto {
return promiseRegisterUseCase.createPromise(promiseRequest)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.depromeet.whatnow.api.promise.dto

import com.depromeet.whatnow.common.vo.PlaceVo
import com.depromeet.whatnow.domains.promise.domain.Promise
import java.time.LocalDateTime

data class PromiseCreateDto(
val title: String,
val mainUserId: Long,
val meetPlace: PlaceVo?,
val endTime: LocalDateTime,
val inviteCode: String,
) {
companion object {
fun of(p: Promise?, inviteCode: String): PromiseCreateDto {
// Check if p is null and handle the null case appropriately
if (p == null) {
// Return a default or placeholder value for PromiseDto
return PromiseCreateDto("", 1L, null, LocalDateTime.now(), "")
}

// Process non-null Promise object
return PromiseCreateDto(p.title, p.mainUserId, p.meetPlace, p.endTime, inviteCode)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.depromeet.whatnow.api.promise.usecase

import com.depromeet.whatnow.annotation.UseCase
import com.depromeet.whatnow.domains.invitecode.adapter.InviteCodeAdapter

@UseCase
class InviteCodeReadUseCase(
val inviteCodeAdapter: InviteCodeAdapter,
) {
fun findInviteCodeByPromiseId(promiseId: Long): String {
return inviteCodeAdapter.findByPromiseId(promiseId).inviteCode
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.depromeet.whatnow.api.promise.usecase

import com.depromeet.whatnow.annotation.UseCase
import com.depromeet.whatnow.api.promise.dto.PromiseCreateDto
import com.depromeet.whatnow.api.promise.dto.PromiseDto
import com.depromeet.whatnow.api.promise.dto.PromiseRequest
import com.depromeet.whatnow.common.vo.PlaceVo
import com.depromeet.whatnow.domains.invitecode.service.InviteCodeDomainService
import com.depromeet.whatnow.domains.promise.adaptor.PromiseAdaptor
import com.depromeet.whatnow.domains.promise.domain.Promise
import com.depromeet.whatnow.domains.promise.service.PromiseDomainService
Expand All @@ -14,9 +16,10 @@ import java.time.LocalDateTime
class PromiseRegisterUseCase(
val promiseAdaptor: PromiseAdaptor,
val promiseDomainService: PromiseDomainService,
val inviteCodeDomainService: InviteCodeDomainService,
) {
@Transactional
fun createPromise(promiseRequest: PromiseRequest): PromiseDto {
fun createPromise(promiseRequest: PromiseRequest): PromiseCreateDto {
val promise = promiseDomainService.save(
Promise(
title = promiseRequest.title,
Expand All @@ -26,7 +29,8 @@ class PromiseRegisterUseCase(
),
)
promise.createPromiseEvent()
return PromiseDto.from(promise)
val inviteCode = inviteCodeDomainService.upsertInviteCode(promise.id!!)
return PromiseCreateDto.of(promise, inviteCode)
}

fun updatePromiseMeetPlace(promiseId: Long, meetPlace: PlaceVo): PromiseDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.depromeet.whatnow.api.promiseuser.dto.PromiseLocationDto
import com.depromeet.whatnow.api.promiseuser.dto.PromiseUserDto
import com.depromeet.whatnow.api.promiseuser.usecase.PromiseUserReadUseCase
import com.depromeet.whatnow.api.promiseuser.usecase.PromiseUserRecordUseCase
import com.depromeet.whatnow.common.aop.verify.InviteCodeLength
import com.depromeet.whatnow.common.vo.CoordinateVo
import com.depromeet.whatnow.domains.promiseuser.domain.PromiseUserType
import io.swagger.v3.oas.annotations.Operation
Expand Down Expand Up @@ -31,6 +32,16 @@ class PromiseUserController(
return promiseUserRecordUseCase.createPromiseUser(promiseId, userId, userLocation)
}

@Operation(summary = "약속 코드로 약속 유저 생성", description = "약속 코드로 약속에 유저가 참여합니다.")
@PostMapping("/promises/users/join")
fun joinPromise(
@InviteCodeLength
@RequestParam("invite-codes")
inviteCode: String,
): PromiseUserDto {
return promiseUserRecordUseCase.createPromiseUserByInviteCode(inviteCode)
}

@Operation(summary = "약속 id로 약속 유저(promiseUser) 조회", description = "약속ID 로 약속 유저를 조회합니다.")
@GetMapping("/promises/{promiseId}/users")
fun getPromiseUser(@PathVariable("promise-id") promiseId: Long): List<PromiseUserDto> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.depromeet.whatnow.api.promiseuser.dto.PromiseUserDto
import com.depromeet.whatnow.common.aop.verify.ActivePromise
import com.depromeet.whatnow.common.vo.CoordinateVo
import com.depromeet.whatnow.config.security.SecurityUtils
import com.depromeet.whatnow.domains.invitecode.adapter.InviteCodeAdapter
import com.depromeet.whatnow.domains.progresshistory.domain.PromiseProgress.DEFAULT
import com.depromeet.whatnow.domains.promise.exception.PromiseNotParticipateException
import com.depromeet.whatnow.domains.promiseuser.domain.PromiseUser
Expand All @@ -16,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional
@UseCase
class PromiseUserRecordUseCase(
val promiseUserDomainService: PromiseUserDomainService,
val inviteCodeAdapter: InviteCodeAdapter,
) {
@Transactional
fun createPromiseUser(promiseId: Long, userId: Long, userLocation: CoordinateVo): PromiseUserDto {
Expand Down Expand Up @@ -61,4 +63,19 @@ class PromiseUserRecordUseCase(

return promiseUsers.map(PromiseLocationDto::from)
}

@Transactional
fun createPromiseUserByInviteCode(inviteCode: String): PromiseUserDto {
val userId = SecurityUtils.currentUserId
val promiseId = inviteCodeAdapter.findByInviteCode(inviteCode).promiseId
return PromiseUserDto.of(
promiseUserDomainService.createPromiseUser(
PromiseUser(
promiseId = promiseId,
userId = userId,
),
),
progress = DEFAULT,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.depromeet.whatnow.api.promise.controller

import com.depromeet.whatnow.api.image.controller.ImageController
import com.depromeet.whatnow.api.promise.usecase.InviteCodeReadUseCase
import com.depromeet.whatnow.api.promise.usecase.PromiseReadUseCase
import com.depromeet.whatnow.api.promise.usecase.PromiseRegisterUseCase
import org.junit.jupiter.api.Test
Expand All @@ -23,6 +24,9 @@ class PromiseControllerTest {
@MockBean
lateinit var promiseRegisterUseCase: PromiseRegisterUseCase

@MockBean
lateinit var inviteCodeReadUseCase: InviteCodeReadUseCase

@Autowired
lateinit var mockMvc: MockMvc

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.depromeet.whatnow.domains.invitecode.service

import com.depromeet.whatnow.consts.INVITE_CODE_EXPIRED_TIME
import com.depromeet.whatnow.domains.invitecode.adapter.InviteCodeAdapter
import com.depromeet.whatnow.domains.invitecode.domain.InviteCodeRedisEntity
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.junit.jupiter.MockitoExtension
import kotlin.test.assertNotEquals

@ExtendWith(MockitoExtension::class)
class InviteCodeDomainServiceTest() {
@Mock
private lateinit var inviteCodeAdapter: InviteCodeAdapter

private lateinit var inviteCodeDomainService: InviteCodeDomainService

@BeforeEach
fun setUp() {
inviteCodeDomainService = InviteCodeDomainService(inviteCodeAdapter)
}

@Test
fun `사전에 저장된 경우 약속 id로 초대 코드를 생성하지 않고 기존 값을 반환한다`() {
// given
val promiseId1 = 1L
val promiseId2 = 2L

`when`(inviteCodeAdapter.findByPromiseId(promiseId1)).thenReturn(InviteCodeRedisEntity(promiseId1, "code1", INVITE_CODE_EXPIRED_TIME))
`when`(inviteCodeAdapter.findByPromiseId(promiseId2)).thenReturn(InviteCodeRedisEntity(promiseId2, "code2", INVITE_CODE_EXPIRED_TIME))

// when
val code1 = inviteCodeDomainService.upsertInviteCode(promiseId1)
val code2 = inviteCodeDomainService.upsertInviteCode(promiseId2)

// then
assertNotEquals(code1, code2)
}

@Test
fun `저장되지 않은 경우 약속 id로 매번 유니크한 초대 코드를 생성한다`() {
// given
val promiseId1 = 1L
val promiseId2 = 2L

// Mock the behavior of inviteCodeAdapter.findByPromiseId to use inviteCodeRepository
`when`(inviteCodeAdapter.findByPromiseId(promiseId1)).thenCallRealMethod()
`when`(inviteCodeAdapter.findByPromiseId(promiseId2)).thenCallRealMethod()

// when
val code1 = inviteCodeDomainService.upsertInviteCode(promiseId1)
val code2 = inviteCodeDomainService.upsertInviteCode(promiseId2)

// then
assertNotEquals(code1, code2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ const val NCP_LOCAL_SEARCH_DISPLAY_COUNT = 10
const val IMAGE_DOMAIN = "https://image.whatnow.kr"
const val ASSERT_IMAGE_DOMAIN = "https://image.whatnow.kr/assert"
const val USER_DEFAULT_PROFILE_IMAGE = "https://image.whatnow.kr/assert/users/default.svg"

const val INVITE_CODE_EXPIRED_TIME = 86400L
const val REDIS_EXPIRE_EVENT_PATTERN = "__keyevent@*__:expired"
const val INVITE_CODE_LENGTH = 13

val SWAGGER_PATTERNS = arrayOf(
"/swagger-resources/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.depromeet.whatnow.common.aop.verify

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class InviteCodeLength
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.depromeet.whatnow.common.aop.verify

import com.depromeet.whatnow.domains.invitecode.exception.InviteCodeFormatMismatchException
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.springframework.stereotype.Component

@Aspect
@Component
class InviteCodeLengthValidationAspect {
@Around("@annotation(com.depromeet.whatnow.common.aop.verify.InviteCodeLength)")
fun validateInviteCodeFormat(joinPoint: ProceedingJoinPoint): Any? {
val args = joinPoint.args
val inviteCode = args[0]
println("inviteCode: $inviteCode")
if (inviteCode is String && inviteCode.length != 13) {
throw InviteCodeFormatMismatchException.EXCEPTION
}
return joinPoint.proceed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.depromeet.whatnow.domains.invitecode.adapter

import com.depromeet.whatnow.annotation.Adapter
import com.depromeet.whatnow.domains.invitecode.domain.InviteCodeRedisEntity
import com.depromeet.whatnow.domains.invitecode.exception.InviteCodeNotFoundException
import com.depromeet.whatnow.domains.invitecode.repository.InviteCodeRepository
import org.springframework.data.repository.findByIdOrNull

@Adapter
class InviteCodeAdapter(
val inviteCodeRepository: InviteCodeRepository,
) {
fun save(inviteCodeRedisEntity: InviteCodeRedisEntity): InviteCodeRedisEntity {
return inviteCodeRepository.save(inviteCodeRedisEntity)
}
fun findByPromiseId(promiseId: Long): InviteCodeRedisEntity {
return inviteCodeRepository.findByIdOrNull(promiseId) ?: throw InviteCodeNotFoundException.EXCEPTION
}

fun findByInviteCode(inviteCode: String): InviteCodeRedisEntity {
return inviteCodeRepository.findByInviteCode(inviteCode) ?: throw InviteCodeNotFoundException.EXCEPTION
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.depromeet.whatnow.domains.invitecode.domain

import org.springframework.data.annotation.Id
import org.springframework.data.redis.core.RedisHash
import org.springframework.data.redis.core.TimeToLive
import org.springframework.data.redis.core.index.Indexed

@RedisHash(value = "inviteCode")
class InviteCodeRedisEntity(
@Id
var promiseId: Long,

@Indexed
var inviteCode: String,

@TimeToLive // TTL
var ttl: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.depromeet.whatnow.domains.invitecode.exception

import com.depromeet.whatnow.exception.WhatnowCodeException

class InviteCodeDuplicateCreatedException : WhatnowCodeException(
InviteCodeErrorCode.INVITE_CODE_DUPLICATE_CREATED,
) {
companion object {
val EXCEPTION: WhatnowCodeException = InviteCodeDuplicateCreatedException()
}
}
Loading

0 comments on commit d19ff0e

Please sign in to comment.