Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions docs/feature/505.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Feature #505: 전화번호 입력 시점 변경

## 개요

회원가입 시 전화번호를 필수에서 선택으로 변경하고, 대여 시점에 전화번호를 수집하도록 변경한다.

## 배경

- 현재: 회원가입 시 전화번호 필수 입력
- 변경: 회원가입 시 전화번호 선택, 대여 시 전화번호 필수 입력

## 변경 사항

### 1. 회원가입 (JoinRequest)

**파일:** `src/main/kotlin/upbrella/be/user/dto/request/JoinRequest.kt`

| 항목 | 현재 | 변경 후 |
|------|------|---------|
| phoneNumber | `@NotBlank` (필수) | nullable (선택) |
| 검증 | 필수 + 패턴 검증 | 입력 시에만 패턴 검증 |

```kotlin
// 변경 전
@field:NotBlank
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String = ""

// 변경 후
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String? = null
```

### 2. User 엔티티

**파일:** `src/main/kotlin/upbrella/be/user/entity/User.kt`

| 항목 | 현재 | 변경 후 |
|------|------|---------|
| phoneNumber | `String` (non-null) | `String?` (nullable) |

```kotlin
// 변경 전
var phoneNumber: String,

// 변경 후
var phoneNumber: String? = null,
```

**추가 메서드:**
```kotlin
fun updatePhoneNumber(phoneNumber: String) {
this.phoneNumber = phoneNumber
}

fun hasPhoneNumber(): Boolean {
return !phoneNumber.isNullOrBlank()
}
```

### 3. 대여 요청 (RentUmbrellaByUserRequest)

**파일:** `src/main/kotlin/upbrella/be/rent/dto/request/RentUmbrellaByUserRequest.kt`

| 항목 | 현재 | 변경 후 |
|------|------|---------|
| phoneNumber | 없음 | 추가 (필수) |

```kotlin
// 변경 후
data class RentUmbrellaByUserRequest(
val region: String? = null,
val storeId: Long = 0,
val umbrellaId: Long = 0,
@field:Size(max = 400, message = "conditionReport는 최대 400자여야 합니다.")
val conditionReport: String? = null,
// 추가
@field:NotBlank(message = "전화번호는 필수입니다.")
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String = ""
)
```

### 4. 대여 서비스 (RentService)

**파일:** `src/main/kotlin/upbrella/be/rent/service/RentService.kt`

`addRental` 메서드에 전화번호 업데이트 로직 추가:

```kotlin
@Transactional
fun addRental(rentUmbrellaByUserRequest: RentUmbrellaByUserRequest, userToRent: User) {
// 기존 검증 로직...

// 전화번호 업데이트 (신규 추가)
if (!userToRent.hasPhoneNumber()) {
userToRent.updatePhoneNumber(rentUmbrellaByUserRequest.phoneNumber)
}

// 기존 대여 로직...
}
```

### 5. 대여 폼 응답 (RentFormResponse)

**파일:** `src/main/kotlin/upbrella/be/rent/dto/response/RentFormResponse.kt`

대여 폼 조회 시 사용자의 전화번호 유무를 반환하여 프론트엔드에서 입력 필드 표시 여부 결정:

```kotlin
data class RentFormResponse(
// 기존 필드들...
val hasPhoneNumber: Boolean // 추가
)
```

## API 변경 사항

### POST /users/join (회원가입)

**Request Body 변경:**
```json
{
"name": "홍길동",
"email": "test@example.com",
"phoneNumber": "010-1234-5678", // 선택 (nullable)
"bank": "신한",
"accountNumber": "110-123-456789"
}
```

### GET /rent/form/{umbrellaId} (대여 폼 조회)

**Response Body 변경:**
```json
{
"code": 200,
"message": "success",
"data": {
"umbrellaUuid": 1,
"storeMetaId": 1,
"classification": "대여소",
"rentStoreName": "스타벅스 강남점",
"hasPhoneNumber": false // 추가
}
}
```

### POST /rent (우산 대여)

**Request Body 변경:**
```json
{
"region": "강남",
"storeId": 1,
"umbrellaId": 1,
"conditionReport": "상태 양호",
"phoneNumber": "010-1234-5678" // 추가 (필수)
}
```

## 영향 받는 파일

| 파일 | 변경 내용 |
|------|----------|
| `JoinRequest.kt` | phoneNumber nullable로 변경 |
| `User.kt` | phoneNumber nullable + 메서드 추가 |
| `RentUmbrellaByUserRequest.kt` | phoneNumber 필드 추가 |
| `RentService.kt` | 전화번호 업데이트 로직 추가 |
| `RentFormResponse.kt` | hasPhoneNumber 필드 추가 |
| `UserInfoResponse.kt` | phoneNumber nullable 처리 |
| `SingleUserInfoResponse.kt` | phoneNumber nullable 처리 |
| `RentalHistoryResponse.kt` | phoneNumber nullable 처리 |
| `HistoryInfoDto.kt` | phoneNumber nullable 처리 |

## 테스트 케이스

### 회원가입
- [ ] 전화번호 없이 회원가입 성공
- [ ] 전화번호 포함하여 회원가입 성공
- [ ] 잘못된 전화번호 형식으로 회원가입 시 실패

### 대여
- [ ] 전화번호 없는 사용자가 대여 시 전화번호 저장됨
- [ ] 전화번호 있는 사용자가 대여 시 기존 전화번호 유지
- [ ] 대여 시 잘못된 전화번호 형식이면 실패

### 대여 폼
- [ ] 전화번호 없는 사용자: `hasPhoneNumber: false`
- [ ] 전화번호 있는 사용자: `hasPhoneNumber: true`

## 마이그레이션

기존 사용자 데이터는 변경 없이 유지됩니다. 신규 가입자부터 적용됩니다.

## 주의사항

1. **하위 호환성**: 기존 API를 사용하는 클라이언트는 대여 요청 시 phoneNumber 필드를 추가해야 합니다.
2. **프론트엔드 연동**: 대여 폼에서 `hasPhoneNumber` 값에 따라 전화번호 입력 필드 표시 여부를 결정해야 합니다.
9 changes: 7 additions & 2 deletions src/main/kotlin/upbrella/be/rent/controller/RentController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ class RentController(
private val log = LoggerFactory.getLogger(RentController::class.java)

@GetMapping("/rent/form/{umbrellaId}")
fun findRentForm(@PathVariable umbrellaId: Long): ResponseEntity<CustomResponse<RentFormResponse>> {
val rentForm = rentService.findRentForm(umbrellaId)
fun findRentForm(
@PathVariable umbrellaId: Long,
httpSession: HttpSession
): ResponseEntity<CustomResponse<RentFormResponse>> {
val user = httpSession.getAttribute("user") as SessionUser
val userToRent = userReader.findUserById(user.id)
val rentForm = rentService.findRentForm(umbrellaId, userToRent)

return ResponseEntity
.ok()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package upbrella.be.rent.dto.request

import javax.validation.constraints.NotBlank
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

data class RentUmbrellaByUserRequest(
Expand All @@ -8,5 +10,10 @@ data class RentUmbrellaByUserRequest(
val umbrellaId: Long = 0,

@field:Size(max = 400, message = "conditionReport는 최대 400자여야 합니다.")
val conditionReport: String? = null
val conditionReport: String? = null,

@field:NotBlank(message = "전화번호는 필수입니다.")
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String = ""
Comment on lines +15 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the default value or reconsider the validation.

The default value of "" (empty string) conflicts with the @NotBlank validation, which requires a non-empty string. If a caller relies on the default, validation will always fail. Consider removing the default value entirely, as phoneNumber is required during rental per the PR objectives.

🔧 Proposed fix
     @field:NotBlank(message = "전화번호는 필수입니다.")
     @field:Size(max = 16)
     @field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
-    val phoneNumber: String = ""
+    val phoneNumber: String
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@field:NotBlank(message = "전화번호는 필수입니다.")
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String = ""
@field:NotBlank(message = "전화번호는 필수입니다.")
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String
🤖 Prompt for AI Agents
In @src/main/kotlin/upbrella/be/rent/dto/request/RentUmbrellaByUserRequest.kt
around lines 15 - 18, The phoneNumber property in RentUmbrellaByUserRequest
currently has a default empty string which conflicts with the @NotBlank
validation; remove the "= \"\"" initializer on the val phoneNumber so the field
is required at construction (or alternatively provide a valid non-blank default
if you intend it optional), ensuring the @field:NotBlank/@field:Pattern
validations can be satisfied.

)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.time.LocalDateTime
data class HistoryInfoDto @QueryProjection constructor(
val id: Long,
val name: String,
val phoneNumber: String,
val phoneNumber: String?,
val rentStoreName: String,
val rentAt: LocalDateTime,
val umbrellaUuid: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ data class RentFormResponse(
val classificationName: String,
val storeMetaId: Long,
val rentStoreName: String,
val umbrellaUuid: Long
val umbrellaUuid: Long,
val hasPhoneNumber: Boolean
) {
companion object {
fun of(umbrella: Umbrella): RentFormResponse {
fun of(umbrella: Umbrella, hasPhoneNumber: Boolean): RentFormResponse {
return RentFormResponse(
classificationName = umbrella.storeMeta.classification!!.name!!,
storeMetaId = umbrella.storeMeta.id!!,
rentStoreName = umbrella.storeMeta.name,
umbrellaUuid = umbrella.uuid
umbrellaUuid = umbrella.uuid,
hasPhoneNumber = hasPhoneNumber
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.time.LocalDateTime
data class RentalHistoryResponse(
val id: Long,
val name: String,
val phoneNumber: String,
val phoneNumber: String?,
val rentStoreName: String,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd kk:mm:ss")
val rentAt: LocalDateTime,
Expand Down
9 changes: 7 additions & 2 deletions src/main/kotlin/upbrella/be/rent/service/RentService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ class RentService(
private val userReader: UserReader,
) {

fun findRentForm(umbrellaId: Long): RentFormResponse {
fun findRentForm(umbrellaId: Long, user: User): RentFormResponse {
val umbrella = umbrellaService.findUmbrellaById(umbrellaId)
if (umbrella.cannotBeRented()) {
throw CannotBeRentedException("[ERROR] 해당 우산은 대여 불가능한 우산입니다.")
}
return RentFormResponse.of(umbrella)
return RentFormResponse.of(umbrella, user.hasPhoneNumber())
}

fun findReturnForm(
Expand All @@ -65,6 +65,11 @@ class RentService(
rentRepository.findByUserIdAndReturnedAtIsNull(userToRent.id).ifPresent {
throw ExistingUmbrellaForRentException("[ERROR] 해당 유저가 대여 중인 우산이 있습니다.")
}

if (!userToRent.hasPhoneNumber()) {
userToRent.updatePhoneNumber(rentUmbrellaByUserRequest.phoneNumber)
}

val umbrella = umbrellaService.findUmbrellaById(rentUmbrellaByUserRequest.umbrellaId)
if (umbrella.storeMeta.id != rentUmbrellaByUserRequest.storeId) {
throw UmbrellaStoreMissMatchException("[ERROR] 해당 우산은 해당 매장에 존재하지 않습니다.")
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/upbrella/be/user/dto/request/JoinRequest.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package upbrella.be.user.dto.request

import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

Expand All @@ -10,10 +9,9 @@ data class JoinRequest(
@field:Pattern(regexp = "^[가-힣a-zA-Z\\s]{2,20}$", message = "이름은 한글, 영문, 공백 2~20자로 입력해주세요.")
val name: String = "",

@field:NotBlank
@field:Size(max = 16)
@field:Pattern(regexp = "^\\d{3}-?\\d{4}-?\\d{4}$", message = "유효한 전화번호 형식이 아닙니다.")
val phoneNumber: String = "",
val phoneNumber: String? = null,

@field:Email
@field:Size(max = 100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.time.LocalDateTime
data class SingleUserInfoResponse(
val id: Long,
val name: String,
val phoneNumber: String,
val phoneNumber: String?,
val email: String,
val bank: String?,
val accountNumber: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import upbrella.be.user.entity.User
data class UserInfoResponse(
val id: Long,
val name: String,
val phoneNumber: String,
val phoneNumber: String?,
val bank: String?,
val accountNumber: String?,
val email: String,
Expand Down
10 changes: 9 additions & 1 deletion src/main/kotlin/upbrella/be/user/entity/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import javax.persistence.Id
class User(
var socialId: Long,
var name: String,
var phoneNumber: String,
var phoneNumber: String? = null,
var email: String,
var provider: String = "KAKAO", // KAKAO or APPLE
var adminStatus: Boolean = false,
Expand Down Expand Up @@ -102,4 +102,12 @@ class User(
fun updateAdminStatus() {
this.adminStatus = !this.adminStatus
}

fun updatePhoneNumber(phoneNumber: String) {
this.phoneNumber = phoneNumber
}

fun hasPhoneNumber(): Boolean {
return !phoneNumber.isNullOrBlank()
}
}
Loading