Skip to content

Commit 1094e70

Browse files
authored
Merge pull request #7 from f-lab-edu/feat/#6-format
공통 응답 및 예외 포맷 세팅
2 parents cc6e3b5 + 48e76fb commit 1094e70

File tree

27 files changed

+517
-66
lines changed

27 files changed

+517
-66
lines changed

database/schema.sql

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,18 @@ CREATE TABLE users
4646
nickname VARCHAR(50) NOT NULL,
4747
phone_number VARCHAR(20) NOT NULL UNIQUE,
4848
birth_date DATE NOT NULL,
49-
gender VARCHAR(5) NOT NULL, # ENUM ('M', 'F')
49+
gender VARCHAR(5) NOT NULL, # ENUM ('M', 'F')
5050
role VARCHAR(20) NOT NULL, # ENUM ('USER', 'SUPPLIER', 'ADMIN')
51-
created_at DATETIME DEFAULT NOW(),
52-
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW()
51+
created_at DATETIME DEFAULT NOW(),
52+
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW()
5353
) ENGINE = InnoDB
5454
DEFAULT CHARSET = utf8mb4;
5555

5656
CREATE TABLE user_term_agreements
5757
(
58-
id BIGINT PRIMARY KEY AUTO_INCREMENT,
59-
user_id BIGINT NOT NULL,
60-
term_id BIGINT NOT NULL,
58+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
59+
user_id BIGINT NOT NULL,
60+
term_id BIGINT NOT NULL,
6161
agreed_at DATETIME DEFAULT NOW(),
6262

6363
FOREIGN KEY (user_id) REFERENCES users (id),
@@ -77,10 +77,11 @@ CREATE TABLE phone_verifications
7777
verification_code VARCHAR(5) NOT NULL,
7878
attempt_count INT NOT NULL DEFAULT 0, # 인증 시도 횟수 (최대 3회 제한)
7979
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
80-
expired_at DATETIME NOT NULL,
81-
verified_at DATETIME NULL,
82-
created_at DATETIME DEFAULT NOW(),
83-
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(),
80+
expired_at DATETIME NOT NULL,
81+
verified_at DATETIME NULL,
82+
last_sent_at DATETIME NOT NULL,
83+
created_at DATETIME DEFAULT NOW(),
84+
updated_at DATETIME DEFAULT NOW() ON UPDATE NOW(),
8485

8586
UNIQUE KEY uk_phone_number (phone_number)
8687
) ENGINE = InnoDB

http/admin-api/term.http

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### 약관 조회 (페이지네이션)
2+
GET http://localhost:8081/v1/admin/terms?page=0&size=10
3+
4+
5+
### 약관 생성
6+
POST http://localhost:8081/v1/admin/terms
7+
Content-Type: application/json
8+
9+
{
10+
"code": "TERM_TEST",
11+
"title": "약관 생성 테스트",
12+
"content": "이 약관은 테스트를 위해 생성되었습니다.",
13+
"displayOrder": 1000,
14+
"isRequired": false,
15+
"activatedAt": "2025-10-03T14:00:00"
16+
}

http/api/signup.http

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
### 회원가입을 위해 현재 활성화된 약관 조회
2+
GET http://localhost:8080/v1/terms/active
3+
4+
### 핸드폰 인증 번호 요청
5+
POST http://localhost:8080/v1/phone-verifications/send
6+
Content-Type: application/json
7+
8+
{
9+
"phoneNumber": "010-2717-8134"
10+
}
11+
12+
### 핸드폰 인증 번호 검증
13+
POST http://localhost:8080/v1/phone-verifications/verify
14+
Content-Type: application/json
15+
16+
{
17+
"phoneNumber": "010-2717-8134",
18+
"verificationCode": "64793"
19+
}
20+
21+
### 회원 가입 요청
22+
POST http://localhost:8080/v1/users/signup
23+
Content-Type: application/json
24+
25+
{
26+
"password": "seungjo123!",
27+
"email": "[email protected]",
28+
"nickname": "f1v3",
29+
"phoneNumber": "010-2717-8134",
30+
"birth": "1999-05-13",
31+
"gender": "M",
32+
"agreedTerms": [
33+
{
34+
"termCode": "TERM_SERVICE",
35+
"version": 2
36+
},
37+
{
38+
"termCode": "TERM_PRIVACY",
39+
"version": 3
40+
},
41+
{
42+
"termCode": "TERM_AGE",
43+
"version": 1
44+
},
45+
{
46+
"termCode": "TERM_MARKETING",
47+
"version": 1
48+
}
49+
]
50+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.f1v3.reservation.admin;
2+
3+
import com.f1v3.reservation.common.api.error.ErrorCode;
4+
import com.f1v3.reservation.common.api.error.ReservationException;
5+
import com.f1v3.reservation.common.api.response.ApiResponse;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.core.Ordered;
9+
import org.springframework.core.annotation.Order;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.MethodArgumentNotValidException;
12+
import org.springframework.web.bind.annotation.ExceptionHandler;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
/**
19+
* 관리자 서버 컨트롤러 어드바이스
20+
*
21+
* @author Seungjo, Jeong
22+
*/
23+
@Slf4j
24+
@RestControllerAdvice
25+
public class AdminReservationControllerAdvice {
26+
27+
@Order(Ordered.HIGHEST_PRECEDENCE)
28+
@ExceptionHandler(ReservationException.class)
29+
public ResponseEntity<ApiResponse<?>> handleReservationException(ReservationException e) {
30+
String logMessage = e.getLogMessage();
31+
e.getLogLevel().accept(logMessage);
32+
33+
return ResponseEntity
34+
.status(e.getStatus().getCode())
35+
.body(ApiResponse.error(e.getCode(), e.getMessage()));
36+
}
37+
38+
@ExceptionHandler(MethodArgumentNotValidException.class)
39+
public ResponseEntity<ApiResponse<?>> handleArgumentNotValidException(
40+
MethodArgumentNotValidException e, HttpServletRequest request) {
41+
42+
ErrorCode errorCode = ErrorCode.INVALID_REQUEST_PARAMETER;
43+
44+
Map<String, String> errors = new HashMap<>();
45+
Map<String, Object> parameters = new HashMap<>();
46+
e.getBindingResult().getFieldErrors().forEach(error -> {
47+
errors.put(error.getField(), error.getDefaultMessage());
48+
parameters.put(error.getField(), error.getRejectedValue());
49+
}
50+
);
51+
52+
log.info("Validation error | URL: {} | Method: {} | Errors: {} | Parameters: {}",
53+
request.getRequestURI(),
54+
request.getMethod(),
55+
errors,
56+
parameters
57+
);
58+
59+
ApiResponse<?> response = ApiResponse.error(
60+
errorCode.getCode(),
61+
errorCode.getMessage(),
62+
errors
63+
);
64+
65+
return ResponseEntity
66+
.status(errorCode.getStatus().getCode())
67+
.body(response);
68+
}
69+
70+
@Order(Ordered.LOWEST_PRECEDENCE)
71+
@ExceptionHandler(Exception.class)
72+
public ResponseEntity<ApiResponse<?>> handleGeneralException(
73+
Exception e, HttpServletRequest request) {
74+
75+
log.error("Unexpected error | URL: {} | Method: {} | Error: {}",
76+
request.getRequestURI(),
77+
request.getMethod(),
78+
e.getMessage(),
79+
e
80+
);
81+
82+
ErrorCode errorCode = ErrorCode.SERVER_ERROR;
83+
return ResponseEntity
84+
.status(errorCode.getStatus().getCode())
85+
.body(ApiResponse.error(errorCode.getCode(), errorCode.getMessage()));
86+
}
87+
}

reservation-admin-api/src/main/java/com/f1v3/reservation/admin/term/TermController.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import com.f1v3.reservation.admin.term.dto.CreateTermRequest;
44
import com.f1v3.reservation.admin.term.dto.CreateTermResponse;
55
import com.f1v3.reservation.admin.term.dto.TermResponse;
6+
import com.f1v3.reservation.common.api.response.ApiResponse;
67
import jakarta.validation.Valid;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.data.domain.Pageable;
910
import org.springframework.data.web.PageableDefault;
1011
import org.springframework.http.HttpStatus;
11-
import org.springframework.http.ResponseEntity;
1212
import org.springframework.web.bind.annotation.*;
1313

1414
import java.util.List;
@@ -31,8 +31,8 @@ public class TermController {
3131
* @param pageable default page = 0, size = 10
3232
*/
3333
@GetMapping
34-
public ResponseEntity<List<TermResponse>> getPagedTerms(@PageableDefault Pageable pageable) {
35-
return ResponseEntity.ok(termService.getPagedTerms(pageable));
34+
public ApiResponse<List<TermResponse>> getPagedTerms(@PageableDefault Pageable pageable) {
35+
return ApiResponse.success(termService.getPagedTerms(pageable));
3636
}
3737

3838
/**
@@ -42,8 +42,8 @@ public ResponseEntity<List<TermResponse>> getPagedTerms(@PageableDefault Pageabl
4242
* @return 생성된 약관의 ID 포함한 응답 DTO
4343
*/
4444
@PostMapping
45-
public ResponseEntity<CreateTermResponse> createTerm(@Valid @RequestBody CreateTermRequest request) {
46-
CreateTermResponse response = termService.create(request);
47-
return ResponseEntity.status(HttpStatus.CREATED).body(response);
45+
@ResponseStatus(HttpStatus.CREATED)
46+
public ApiResponse<CreateTermResponse> createTerm(@Valid @RequestBody CreateTermRequest request) {
47+
return ApiResponse.success(termService.create(request));
4848
}
4949
}

reservation-admin-api/src/main/java/com/f1v3/reservation/admin/term/TermService.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.f1v3.reservation.admin.term.dto.CreateTermRequest;
44
import com.f1v3.reservation.admin.term.dto.CreateTermResponse;
55
import com.f1v3.reservation.admin.term.dto.TermResponse;
6+
import com.f1v3.reservation.common.api.error.ErrorCode;
7+
import com.f1v3.reservation.common.api.error.ReservationException;
68
import com.f1v3.reservation.common.domain.term.Term;
79
import com.f1v3.reservation.common.domain.term.dto.AdminTermDto;
810
import com.f1v3.reservation.common.domain.term.enums.TermCode;
@@ -15,6 +17,9 @@
1517
import org.springframework.transaction.annotation.Transactional;
1618

1719
import java.util.List;
20+
import java.util.Map;
21+
22+
import static com.f1v3.reservation.common.api.error.ErrorCode.TERM_CODE_INVALID;
1823

1924
/**
2025
* 관리자용 약관 서비스
@@ -39,7 +44,7 @@ public List<TermResponse> getPagedTerms(Pageable pageable) {
3944
@Transactional
4045
public CreateTermResponse create(CreateTermRequest request) {
4146
TermCode termCode = TermCode.getCode(request.code())
42-
.orElseThrow(() -> new IllegalArgumentException("Invalid term code: " + request.code()));
47+
.orElseThrow(() -> new ReservationException(TERM_CODE_INVALID, log::warn));
4348

4449
int nextVersion = termRepository.findMaxVersionByCode(termCode)
4550
.map(version -> version + 1)
@@ -68,8 +73,12 @@ private Term saveWithConstraintCheck(Term term) {
6873
try {
6974
return termRepository.save(term);
7075
} catch (DataIntegrityViolationException e) {
71-
log.error("Term constraint violation: {}", e.getMessage());
72-
throw new IllegalStateException("Term constraint violation", e);
76+
Map<String, Object> parameters = Map.of(
77+
"termCode", term.getCode(),
78+
"termVersion", term.getVersion()
79+
);
80+
81+
throw new ReservationException(ErrorCode.TERM_VERSION_CONSTRAINT_VIOLATION, log::error, parameters, e);
7382
}
7483
}
7584
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.f1v3.reservation.api;
2+
3+
import com.f1v3.reservation.common.api.error.ErrorCode;
4+
import com.f1v3.reservation.common.api.error.ReservationException;
5+
import com.f1v3.reservation.common.api.response.ApiResponse;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.core.Ordered;
9+
import org.springframework.core.annotation.Order;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.MethodArgumentNotValidException;
12+
import org.springframework.web.bind.annotation.ExceptionHandler;
13+
import org.springframework.web.bind.annotation.RestControllerAdvice;
14+
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
/**
19+
* 사용자 서버 컨트롤러 어드바이스
20+
*
21+
* @author Seungjo, Jeong
22+
*/
23+
@Slf4j
24+
@RestControllerAdvice
25+
public class ReservationControllerAdvice {
26+
27+
@Order(Ordered.HIGHEST_PRECEDENCE)
28+
@ExceptionHandler(ReservationException.class)
29+
public ResponseEntity<ApiResponse<?>> handleReservationException(ReservationException e) {
30+
String logMessage = e.getLogMessage();
31+
e.getLogLevel().accept(logMessage);
32+
33+
return ResponseEntity
34+
.status(e.getStatus().getCode())
35+
.body(ApiResponse.error(e.getCode(), e.getMessage()));
36+
}
37+
38+
@ExceptionHandler(MethodArgumentNotValidException.class)
39+
public ResponseEntity<ApiResponse<?>> handleArgumentNotValidException(
40+
MethodArgumentNotValidException e, HttpServletRequest request) {
41+
42+
ErrorCode code = ErrorCode.INVALID_REQUEST_PARAMETER;
43+
44+
Map<String, String> errors = new HashMap<>();
45+
Map<String, Object> parameters = new HashMap<>();
46+
e.getBindingResult().getFieldErrors().forEach(error -> {
47+
errors.put(error.getField(), error.getDefaultMessage());
48+
parameters.put(error.getField(), error.getRejectedValue());
49+
}
50+
);
51+
52+
log.info("Validation error | URI = {} | Method = {} | Errors = {} | Parameters = {}",
53+
request.getRequestURI(),
54+
request.getMethod(),
55+
errors,
56+
parameters
57+
);
58+
59+
ApiResponse<?> response = ApiResponse.error(
60+
ErrorCode.INVALID_REQUEST_PARAMETER.getCode(),
61+
ErrorCode.INVALID_REQUEST_PARAMETER.getMessage(),
62+
errors
63+
);
64+
65+
return ResponseEntity
66+
.status(code.getStatus().getCode())
67+
.body(response);
68+
}
69+
70+
@Order(Ordered.LOWEST_PRECEDENCE)
71+
@ExceptionHandler(Exception.class)
72+
public ResponseEntity<ApiResponse<?>> handleGeneralException(
73+
Exception e, HttpServletRequest request) {
74+
75+
log.error("Unexpected error | URL = {} | Method = {} | Message = {}",
76+
request.getRequestURI(),
77+
request.getMethod(),
78+
e.getMessage(),
79+
e
80+
);
81+
82+
ErrorCode errorCode = ErrorCode.SERVER_ERROR;
83+
return ResponseEntity
84+
.status(errorCode.getStatus().getCode())
85+
.body(ApiResponse.error(errorCode.getCode(), errorCode.getMessage()));
86+
}
87+
}

0 commit comments

Comments
 (0)