[#22] 프론트엔드 React 전환 및 인증 API 수정 #23

Merged
pricelees merged 9 commits from refactor/#22 into main 2025-07-27 03:39:20 +00:00
7 changed files with 135 additions and 18 deletions
Showing only changes of commit 15126bc884 - Show all commits

View File

@ -7,8 +7,8 @@ import roomescape.member.exception.MemberErrorCode
import roomescape.member.exception.MemberException import roomescape.member.exception.MemberException
import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.web.MemberRetrieveListResponse import roomescape.member.infrastructure.persistence.Role
import roomescape.member.web.toRetrieveResponse import roomescape.member.web.*
@Service @Service
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -27,6 +27,21 @@ class MemberService(
memberRepository.findByEmailAndPassword(email, password) 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 { private fun fetchOrThrow(block: () -> MemberEntity?): MemberEntity {
return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND) return block() ?: throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
} }

View File

@ -5,9 +5,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody
import roomescape.auth.web.support.Admin import roomescape.auth.web.support.Admin
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.web.MemberRetrieveListResponse import roomescape.member.web.MemberRetrieveListResponse
import roomescape.member.web.SignupRequest
import roomescape.member.web.SignupResponse
@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") @Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.")
interface MemberAPI { interface MemberAPI {
@ -21,4 +24,14 @@ interface MemberAPI {
) )
) )
fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>>
@Operation(summary = "회원 가입")
@ApiResponses(
ApiResponse(
responseCode = "201",
description = "성공",
useReturnTypeSchema = true
)
)
fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>>
} }

View File

@ -8,5 +8,6 @@ enum class MemberErrorCode(
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요.") MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.")
} }

View File

@ -4,4 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository
interface MemberRepository : JpaRepository<MemberEntity, Long> { interface MemberRepository : JpaRepository<MemberEntity, Long> {
fun findByEmailAndPassword(email: String, password: String): MemberEntity? fun findByEmailAndPassword(email: String, password: String): MemberEntity?
fun findByEmail(email: String): MemberEntity?
} }

View File

@ -2,16 +2,26 @@ package roomescape.member.web
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping 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 org.springframework.web.bind.annotation.RestController
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.docs.MemberAPI import roomescape.member.docs.MemberAPI
import java.net.URI
@RestController @RestController
class MemberController( class MemberController(
private val memberService: MemberService private val memberService: MemberService
) : MemberAPI { ) : MemberAPI {
@PostMapping("/members")
override fun signup(@RequestBody request: SignupRequest): ResponseEntity<CommonApiResponse<SignupResponse>> {
val response: SignupResponse = memberService.create(request)
return ResponseEntity.created(URI.create("/members/${response.id}"))
.body(CommonApiResponse(response))
}
@GetMapping("/members") @GetMapping("/members")
override fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> { override fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> {
val response: MemberRetrieveListResponse = memberService.findMembers() val response: MemberRetrieveListResponse = memberService.findMembers()

View File

@ -19,3 +19,19 @@ data class MemberRetrieveResponse(
data class MemberRetrieveListResponse( data class MemberRetrieveListResponse(
val members: List<MemberRetrieveResponse> val members: List<MemberRetrieveResponse>
) )
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
)

View File

@ -4,12 +4,16 @@ import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import roomescape.auth.exception.AuthErrorCode 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.MemberController
import roomescape.member.web.MemberRetrieveListResponse import roomescape.member.web.MemberRetrieveListResponse
import roomescape.member.web.SignupRequest
import roomescape.util.MemberFixture import roomescape.util.MemberFixture
import roomescape.util.RoomescapeApiTest import roomescape.util.RoomescapeApiTest
import kotlin.random.Random 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) }
}
}
}
}
} }
} }