-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[1 - 3단계 방탈출 사용자 예약] 이든(최승준) 미션 제출합니다. #57
Changes from 31 commits
bd5a99e
b0bd9bb
ba6b536
3592c1d
75c7ee4
ec2e3b0
e37964d
a8a4a02
0fe2794
540fdee
5d6008e
7307152
4f592bd
7331554
17af3db
9e8790e
e951bbb
3bbd400
40d021b
ff72e78
fe93600
60d7ed3
478963c
45fbf52
efb21f8
f7177bd
1f76b8f
772631f
ac95a28
830df0c
cd175dc
2d9aa37
23b6a97
35935dc
5604c83
230ce75
4cec0d8
04131d6
a0899cf
7b816fd
4a9a980
0f8ef2c
0470138
c983dfd
41fa7a2
818b1eb
a2905fc
79de6f5
604a0c0
a0f45d0
15276bd
b40b582
7620bc5
f6b6e37
4014865
80690a0
272a414
3175c22
81687e0
bc3aaf6
a80fcbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,30 @@ | ||
# 방탈출 예약 관리 | ||
|
||
## 응답 페이지 | ||
|
||
- `/` : 웰컴 페이지 | ||
- `/admin` : 어드민 메인 페이지 | ||
- `/admin/reservation` : 예약 관리 페이지 | ||
- `/admin/time` : 예약 시간 관리 페이지 | ||
|
||
## API 명세 | ||
|
||
### 예약 목록 조회 API | ||
| Method | Endpoint | Description | File Path | Controller Type | | ||
|--------|---------------------------------------|-----------------------|----------------------------------------|-------------------| | ||
| GET | `/` | 인기 테마 페이지 요청 | `templates/index.html` | `@Controller` | | ||
| GET | `/reservation` | 사용자 예약 페이지 요청 | `templates/reservation.html` | `@Controller` | | ||
| GET | `/admin` | 어드민 페이지 요청 | `templates/admin/index.html` | `@Controller` | | ||
| GET | `/admin/reservation` | 예약 관리 페이지 요청 | `templates/admin/reservation-new.html` | `@Controller` | | ||
| GET | `/admin/time` | 예약 시간 관리 페이지 요청 | `templates/admin/time.html` | `@Controller` | | ||
| GET | `/admin/theme` | 테마 관리 페이지 요청 | `templates/admin/theme.html` | `@Controller` | | ||
| GET | `/reservations` | 예약 정보 조회 | | `@RestController` | | ||
| GET | `/reservations/themes/{themeId}?date` | 특정 날짜의 특정 테마 예약 정보 조회 | | `@RestController` | | ||
| POST | `/reservations` | 예약 추가 | | `@RestController` | | ||
| DELETE | `/reservations/{id}` | 예약 취소 | | `@RestController` | | ||
| GET | `/times` | 예약 시간 조회 | | `@RestController` | | ||
| DELETE | `/times/{id}` | 예약 시간 추가 | | `@RestController` | | ||
| POST | `/times` | 예약 시간 삭제 | | `@RestController` | | ||
| GET | `/themes` | 테마 정보 조회 | | `@RestController` | | ||
| GET | `/themes/top?count` | 인기순 테마 조회 | | `@RestController` | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분도 Endpoint를 설계할 때 너무 고민이 됐어요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 설계했다면 themes의 sort option (TOP을 추가) 과 페이지네이션 요청객체를 조합해서 응답하긴 했을거 같아요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그렇게도 처리해줄 수 있겠네요:) |
||
| POST | `/themes` | 테마 추가 | | `@RestController` | | ||
| DELETE | `/themes/{id}` | 테마 삭제 | | `@RestController` | | ||
|
||
--- | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 꼼꼼한 API 문서네요 ㅎ 👍 |
||
### 예약 정보 조회 API | ||
|
||
- Request | ||
|
||
|
@@ -36,6 +51,37 @@ Content-Type: application/json | |
] | ||
``` | ||
|
||
--- | ||
|
||
### 특정 날짜의 특정 테마 예약 정보 조회 | ||
|
||
- Request | ||
|
||
``` | ||
GET /reservations/themes/1?date=2024-12-31 HTTP/1.1 | ||
``` | ||
|
||
--- | ||
|
||
- Response | ||
|
||
``` | ||
[ | ||
{ | ||
"timeId": 1, | ||
"startAt": "17:00", | ||
"alreadyBooked": false | ||
}, | ||
{ | ||
"timeId": 2, | ||
"startAt": "20:00", | ||
"alreadyBooked": false | ||
} | ||
] | ||
``` | ||
|
||
--- | ||
|
||
### 예약 추가 API | ||
|
||
- Request | ||
|
@@ -68,6 +114,8 @@ Content-Type: application/json | |
} | ||
``` | ||
|
||
--- | ||
|
||
### 예약 취소 API | ||
|
||
- Request | ||
|
@@ -82,7 +130,9 @@ DELETE /reservations/1 HTTP/1.1 | |
HTTP/1.1 204 | ||
``` | ||
|
||
### 시간 목록 조회 API | ||
--- | ||
|
||
### 예약 시간 조회 API | ||
|
||
- Request | ||
|
||
|
@@ -104,7 +154,9 @@ Content-Type: application/json | |
] | ||
``` | ||
|
||
### 시간 추가 API | ||
--- | ||
|
||
### 예약 시간 추가 API | ||
|
||
- Request | ||
|
||
|
@@ -129,7 +181,9 @@ Content-Type: application/json | |
} | ||
``` | ||
|
||
### 시간 삭제 API | ||
--- | ||
|
||
### 예약 시간 삭제 API | ||
|
||
- Request | ||
|
||
|
@@ -141,4 +195,111 @@ DELETE /times/1 HTTP/1.1 | |
|
||
``` | ||
HTTP/1.1 204 | ||
``` | ||
``` | ||
|
||
--- | ||
|
||
### 테마 정보 조회 API | ||
|
||
- Request | ||
|
||
``` | ||
GET /themes HTTP/1.1 | ||
``` | ||
|
||
- response | ||
|
||
``` | ||
HTTP/1.1 200 | ||
Content-Type: application/json | ||
|
||
[ | ||
{ | ||
"id": 1, | ||
"name": "레벨2 탈출", | ||
"description": "우테코 레벨2를 탈출하는 내용입니다.", | ||
"thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" | ||
} | ||
] | ||
``` | ||
|
||
--- | ||
|
||
### 인기순 테마 조회 API | ||
|
||
- Request | ||
|
||
``` | ||
GET /themes/top?count=10 HTTP/1.1 | ||
``` | ||
|
||
- response | ||
|
||
``` | ||
HTTP/1.1 200 | ||
Content-Type: application/json | ||
|
||
[ | ||
{ | ||
"id": 1, | ||
"name": "레벨2 탈출", | ||
"description": "우테코 레벨2를 탈출하는 내용입니다.", | ||
"thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" | ||
}, | ||
...8개 생략 | ||
{ | ||
"id": 10, | ||
"name": "레벨10 탈출", | ||
"description": "우테코 레벨10를 탈출하는 내용입니다.", | ||
"thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" | ||
} | ||
] | ||
``` | ||
|
||
--- | ||
|
||
### 테마 추가 API | ||
|
||
- Request | ||
|
||
``` | ||
POST /themes HTTP/1.1 | ||
content-type: application/json | ||
|
||
{ | ||
"name": "레벨2 탈출", | ||
"description": "우테코 레벨2를 탈출하는 내용입니다.", | ||
"thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" | ||
} | ||
``` | ||
|
||
- response | ||
|
||
``` | ||
HTTP/1.1 201 | ||
Location: /themes/1 | ||
Content-Type: application/json | ||
|
||
{ | ||
"id": 1, | ||
"name": "레벨2 탈출", | ||
"description": "우테코 레벨2를 탈출하는 내용입니다.", | ||
"thumbnail": "https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg" | ||
} | ||
``` | ||
|
||
--- | ||
|
||
### 테마 삭제 API | ||
|
||
- Request | ||
|
||
``` | ||
DELETE /themes/1 HTTP/1.1 | ||
``` | ||
|
||
- response | ||
|
||
``` | ||
HTTP/1.1 204 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
## 예외 상황 | ||
|
||
### 예약 | ||
|
||
- [x] 예약 생성 시 예약자명, 날짜, 시간에 유효하지 않은 값이 입력할 수 없다. | ||
- null, 빈 값, 날짜 / 시간 포맷 | ||
- [x] 중복 예약 추가는 불가능하다. | ||
- [x] 지나간 날짜와 시간에 대한 예약 생성은 불가능하다. | ||
|
||
### 시간 | ||
|
||
- [x] 시간 생성 시 시작 시간에 유효하지 않은 값이 입력할 수 없다. | ||
- null, 빈 값, 날짜 / 시간 포맷 | ||
- [x] 중복 시간 추가는 불가능하다. | ||
- [x] 특정 시간에 대한 예약이 존재하면, 그 시간을 삭제할 수 없다. | ||
|
||
### 공통 | ||
|
||
- [ ] 존재하지 않는 API로 요청을 보낼 수 없다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
distributionBase=GRADLE_USER_HOME | ||
distributionPath=wrapper/dists | ||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip | ||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip | ||
zipStoreBase=GRADLE_USER_HOME | ||
zipStorePath=wrapper/dists |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package roomescape.controller; | ||
|
||
import org.springframework.stereotype.Controller; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
|
||
@Controller | ||
public class ClientController { | ||
|
||
@GetMapping("/") | ||
public String readPopularThemePage() { | ||
return "index"; | ||
} | ||
|
||
@GetMapping("/reservation") | ||
public String readReservationPage() { | ||
return "reservation"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,19 @@ | ||
package roomescape.controller; | ||
|
||
import java.net.URI; | ||
import java.time.LocalDate; | ||
import java.util.List; | ||
import org.springframework.format.annotation.DateTimeFormat; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import roomescape.dto.reservation.ReservationAvailableTimeResponse; | ||
import roomescape.dto.reservation.ReservationRequest; | ||
import roomescape.dto.reservation.ReservationResponse; | ||
import roomescape.service.ReservationService; | ||
|
@@ -31,6 +35,15 @@ public ResponseEntity<List<ReservationResponse>> readReservations() { | |
return ResponseEntity.ok(reservationResponses); | ||
} | ||
|
||
@GetMapping("/themes/{themeId}") | ||
public ResponseEntity<List<ReservationAvailableTimeResponse>> readAvailableTimeReservations( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 List로 return하는 것도 괜찮을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이전 크루의 리뷰를 복붙 한번 하겠습니당. 응답객체는 Collection이 아닌 Object 형식으로 내려주는게 유연한 설계입니다. 최근에 끄덕끄덕하면서 본 아티클인데, Don’t return arrays as top-level responses. 라는 조언이 이 리뷰에 해당합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 지금처럼 List로 전송하는 API가 있을 때, 거기까지는 생각해보지 못했는데 정말 좋은 의견인 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이때 Endpoint를 통해 버전을 관리하는 방식(ex. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 부분이 말씀해주시는 API 버저닝과 관계있습니다 ㅎ 외부사에 API를 제공하는 경우 API 버전을 올렸다고 맞춰달라고 하는것조차 쉽지 않기 때문입니다 (상대 회사의 개발력을 투입해야함) 인하우스 기능(사내 서버만 활용하는 API)이라고 쉽냐고 물어보면 그렇지도 않습니다. 회사 전체 채널로 3개월씩 공지해도 각자의 일로 바쁘므로 하위버전 API를 계속 사용하는 팀이 나옵니다 손쉬운 해결책으로 보여도 매우 고난한 길이니 가급적 가시지 않기를 권합니다 ㅋㅋ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 실무의 세계란 정말 고려할 사항들이 많네요! |
||
@PathVariable | ||
Long themeId, | ||
@RequestParam | ||
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 개행이 많이 어색해서 변경해보겠습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넹 ㅎ 가독성은 꼼꼼히 챙겨주세요. |
||
return ResponseEntity.ok(reservationService.findReservationByDateAndThemeId(date, themeId)); | ||
} | ||
|
||
@PostMapping | ||
public ResponseEntity<ReservationResponse> createReservation(@RequestBody ReservationRequest reservationRequest) { | ||
ReservationResponse reservationResponse = reservationService.createReservation(reservationRequest); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package roomescape.controller; | ||
|
||
import java.net.URI; | ||
import java.util.List; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.DeleteMapping; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import roomescape.dto.theme.ThemeRequest; | ||
import roomescape.dto.theme.ThemeResponse; | ||
import roomescape.service.ThemeService; | ||
|
||
@RestController | ||
@RequestMapping("/themes") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재는 모든 컨트롤러 코드들이 이렇게 공통 endpoint를 묶어 관리하고 있는 형태인데요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋ 동의합니다 |
||
public class ThemeController { | ||
|
||
private final ThemeService themeService; | ||
|
||
public ThemeController(ThemeService themeService) { | ||
this.themeService = themeService; | ||
} | ||
|
||
@GetMapping | ||
public ResponseEntity<List<ThemeResponse>> readThemes() { | ||
List<ThemeResponse> themeResponses = themeService.findAllThemes(); | ||
|
||
return ResponseEntity.ok(themeResponses); | ||
} | ||
|
||
@GetMapping("/top") | ||
public ResponseEntity<List<ThemeResponse>> readTopNThemes(@RequestParam int count) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재는 거의 모든 메서드의 인자에 final 처리가 되어있지 않은 상태인데요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전 모든 파라미터 / 지역 변수 final에 대해 부정적인 편입니다 인자 final 규칙은 모든 코드에 적용해야만 비로소 효과가 생기는데 (final이 붙어 있지 않으면 가변임을 예상할 수 있음) 자동으로 처리가 안 되다 보니 팀원들에게 강요하기가 어렵습니다. 의식하고 있어도 자주 빠지기도 하구요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 다 처리해줄 게 아니면 오히려 의도가 불분명한 코드가 될 수도 있다는 생각이 있는데요 인텔리제이에 자동으로 final 키워드를 추가해주는 기능이 있기도하고 개인적으로 사이드이펙트를 방지하는데에 좋다는 생각이 들어서 |
||
List<ThemeResponse> themeResponses = themeService.findTopNThemes(count); | ||
|
||
return ResponseEntity.ok(themeResponses); | ||
} | ||
|
||
@PostMapping | ||
public ResponseEntity<ThemeResponse> createTheme(@RequestBody ThemeRequest request) { | ||
ThemeResponse response = themeService.createTheme(request); | ||
|
||
return ResponseEntity.created(URI.create("/themes/" + response.id())) | ||
.body(response); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteTheme(@PathVariable Long id) { | ||
themeService.deleteTheme(id); | ||
|
||
return ResponseEntity.noContent().build(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API URI를 최대한 RESTful하게 설계해보려고 노력했는데요.
그 중에서도
/reservations
컬렉션 내부에 서브 컬렉션인/themes
를 넣어준 이 URI Endpoint는 어떻게 설계하면 좋을 지 고민이 많이 되었던 부분입니다.자원을 적절하게 명시해주고 있는 지, 그리고 변경해야할 것 같다면 어떻게 변경하면 좋을 지 웨지의 의견이 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reservation 와 theme는 종속관계로 생각하시는데 times는 별도로 다루는 이유가 있을까요?
리소스간 관계 설정에 통일성이 있었으면 좋겠습니다.
전 API 설계는 적당히 프론트랑 저희 팀원들만 알아볼 수 만 있으면 된다는 생각입니다.
그리고 전 요구사항 변경이 매우 빈번한 서비스를 관리하는 개발자기 때문에 서비스의 하위호환성을 중시하는 경항성이 있습니다. 그래서 API를 최초에 완전하게 설계하는 것에는 큰 관심이 없고 하위버전과 신규버전의 API를 호환해서 운영하는 것에 관심이 많습니다.
그래서 reservtions prefix가 있냐 마냐는 저에겐 이러든 저러든 큰 상관은 없다고 생각하나, 통일성 없는 규칙은 모든 사람(2주 후의 '나'를 포함한)에게 혼란을 주므로 통일감을 주는건 중요하다고 생각합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
결론적으로 얻고자하는건 시간까지 포함된 예약 여부와 예약 시간들인데
times
가 빠지니 어색하게 느껴지긴 하네요!충분히 고려하지 못한 것 같네요 🥲 짚어주셔서 감사합니다!
/reservations/themes/{themeId}/times?date
이렇게 변경해보면 좋을까요?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 해당 리뷰는 제 착각에서 비롯된 것도 있네요 죄송합니다 🙇♂️
잘못 생각한 부분 ->
/reservations/themes
만 있고/themes
는 없음/times
는 있음-> 리소스 상 theme는 reservation의 하위 개념으로 명시되는데 times는 별도 리소스로 취급한 것으로 생각
그런데 말씀주신것처럼 해당 API의 응답 결과가 themes가 아닌 theme의 시간인거 같아서 말씀주신 방향으로 수정하는게 좋을거 같네요 (뒷걸음질 치다 쥐 잡았네요ㅎ)