diff --git a/src/main/kotlin/roomescape/member/business/MemberService.kt b/src/main/kotlin/roomescape/member/business/MemberService.kt index 8ff4072e..3f5e112d 100644 --- a/src/main/kotlin/roomescape/member/business/MemberService.kt +++ b/src/main/kotlin/roomescape/member/business/MemberService.kt @@ -7,16 +7,16 @@ import roomescape.member.exception.MemberErrorCode import roomescape.member.exception.MemberException import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberRepository -import roomescape.member.web.MemberRetrieveListResponse -import roomescape.member.web.toRetrieveResponse +import roomescape.member.infrastructure.persistence.Role +import roomescape.member.web.* @Service @Transactional(readOnly = true) class MemberService( - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository ) { fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse( - members = memberRepository.findAll().map { it.toRetrieveResponse() } + members = memberRepository.findAll().map { it.toRetrieveResponse() } ) fun findById(memberId: Long): MemberEntity = fetchOrThrow { @@ -27,6 +27,21 @@ class MemberService( memberRepository.findByEmailAndPassword(email, password) } + @Transactional + fun create(request: SignupRequest): SignupResponse { + memberRepository.findByEmail(request.email)?.let { + throw MemberException(MemberErrorCode.DUPLICATE_EMAIL) + } + + val member = MemberEntity( + name = request.name, + email = request.email, + password = request.password, + role = Role.MEMBER + ) + return memberRepository.save(member).toSignupResponse() + } + private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity { return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND) } diff --git a/src/main/kotlin/roomescape/member/docs/MemberAPI.kt b/src/main/kotlin/roomescape/member/docs/MemberAPI.kt index 9428cde2..36ebfa7a 100644 --- a/src/main/kotlin/roomescape/member/docs/MemberAPI.kt +++ b/src/main/kotlin/roomescape/member/docs/MemberAPI.kt @@ -5,20 +5,33 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RequestBody import roomescape.auth.web.support.Admin import roomescape.common.dto.response.CommonApiResponse import roomescape.member.web.MemberRetrieveListResponse +import roomescape.member.web.SignupRequest +import roomescape.member.web.SignupResponse @Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") interface MemberAPI { @Admin @Operation(summary = "모든 회원 조회", tags = ["관리자 로그인이 필요한 API"]) @ApiResponses( - ApiResponse( - responseCode = "200", - description = "성공", - useReturnTypeSchema = true - ) + ApiResponse( + responseCode = "200", + description = "성공", + useReturnTypeSchema = true + ) ) fun findMembers(): ResponseEntity> + + @Operation(summary = "회원 가입") + @ApiResponses( + ApiResponse( + responseCode = "201", + description = "성공", + useReturnTypeSchema = true + ) + ) + fun signup(@RequestBody request: SignupRequest): ResponseEntity> } diff --git a/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt b/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt index 3b365311..daf8d9d0 100644 --- a/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt +++ b/src/main/kotlin/roomescape/member/exception/MemberErrorCode.kt @@ -8,5 +8,6 @@ enum class MemberErrorCode( override val errorCode: String, override val message: String ) : ErrorCode { - MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요.") + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."), + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.") } diff --git a/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt b/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt index 667d9df8..13e2ac6d 100644 --- a/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt +++ b/src/main/kotlin/roomescape/member/infrastructure/persistence/MemberRepository.kt @@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository interface MemberRepository : JpaRepository { fun findByEmailAndPassword(email: String, password: String): MemberEntity? + + fun findByEmail(email: String): MemberEntity? } diff --git a/src/main/kotlin/roomescape/member/web/MemberController.kt b/src/main/kotlin/roomescape/member/web/MemberController.kt index b86d341e..65594ec7 100644 --- a/src/main/kotlin/roomescape/member/web/MemberController.kt +++ b/src/main/kotlin/roomescape/member/web/MemberController.kt @@ -2,16 +2,26 @@ package roomescape.member.web import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController import roomescape.common.dto.response.CommonApiResponse import roomescape.member.business.MemberService import roomescape.member.docs.MemberAPI +import java.net.URI @RestController class MemberController( - private val memberService: MemberService + private val memberService: MemberService ) : MemberAPI { + @PostMapping("/members") + override fun signup(@RequestBody request: SignupRequest): ResponseEntity> { + val response: SignupResponse = memberService.create(request) + return ResponseEntity.created(URI.create("/members/${response.id}")) + .body(CommonApiResponse(response)) + } + @GetMapping("/members") override fun findMembers(): ResponseEntity> { val response: MemberRetrieveListResponse = memberService.findMembers() diff --git a/src/main/kotlin/roomescape/member/web/MemberDTO.kt b/src/main/kotlin/roomescape/member/web/MemberDTO.kt index 00c00c8f..07e76551 100644 --- a/src/main/kotlin/roomescape/member/web/MemberDTO.kt +++ b/src/main/kotlin/roomescape/member/web/MemberDTO.kt @@ -4,18 +4,34 @@ import io.swagger.v3.oas.annotations.media.Schema import roomescape.member.infrastructure.persistence.MemberEntity fun MemberEntity.toRetrieveResponse(): MemberRetrieveResponse = MemberRetrieveResponse( - id = id!!, - name = name + id = id!!, + name = name ) data class MemberRetrieveResponse( - @Schema(description = "회원 식별자") - val id: Long, + @Schema(description = "회원 식별자") + val id: Long, - @Schema(description = "회원 이름") - val name: String + @Schema(description = "회원 이름") + val name: String ) data class MemberRetrieveListResponse( - val members: List + val members: List +) + +data class SignupRequest( + val email: String, + val password: String, + val name: String +) + +data class SignupResponse( + val id: Long, + val name: String, +) + +fun MemberEntity.toSignupResponse(): SignupResponse = SignupResponse( + id = this.id!!, + name = this.name ) diff --git a/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt b/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt index 176b2aea..1d87a7d7 100644 --- a/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt +++ b/src/test/kotlin/roomescape/member/controller/MemberControllerTest.kt @@ -4,12 +4,16 @@ import io.kotest.assertions.assertSoftly import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.mockk.every +import io.mockk.mockk import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.test.web.servlet.MockMvc import roomescape.auth.exception.AuthErrorCode +import roomescape.member.exception.MemberErrorCode +import roomescape.member.infrastructure.persistence.Role import roomescape.member.web.MemberController import roomescape.member.web.MemberRetrieveListResponse +import roomescape.member.web.SignupRequest import roomescape.util.MemberFixture import roomescape.util.RoomescapeApiTest import kotlin.random.Random @@ -82,5 +86,61 @@ class MemberControllerTest( } } } + + given("POST /members") { + val endpoint = "/members" + val request = SignupRequest( + name = "name", + email = "email@email.com", + password = "password" + ) + `when`("같은 이메일이 없으면") { + every { + memberRepository.findByEmail(request.email) + } returns null + + every { + memberRepository.save(any()) + } returns MemberFixture.create( + id = 1, + name = request.name, + account = request.email, + password = request.password, + role = Role.MEMBER + ) + + then("id과 이름을 담아 성공 응답") { + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request + ) { + status { isCreated() } + jsonPath("$.data.name") { value(request.name) } + jsonPath("$.data.id") { value(1) } + } + } + } + + `when`("같은 이메일이 있으면") { + every { + memberRepository.findByEmail(request.email) + } returns mockk() + + then("에러 응답") { + val expectedError = MemberErrorCode.DUPLICATE_EMAIL + + runPostTest( + mockMvc = mockMvc, + endpoint = endpoint, + body = request + ) { + status { isEqualTo(expectedError.httpStatus.value()) } + jsonPath("$.code") { value(expectedError.errorCode) } + } + + } + } + } } }