Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
045b3d1
docs: README.md μž‘μ„±
haeseonlee May 12, 2026
8b5f78a
feat: `@RestControllerAdvice`둜 μ „μ—­ 에외 처리 μΆ”κ°€
haeseonlee May 12, 2026
1f28ca5
feat: 500 μ—λŸ¬κ°€ μ‚¬μš©μžμ—κ²Œ λ…ΈμΆœλ˜μ§€ μ•Šλ„λ‘ 처리 및 μ• λŸ¬ 응닡 JSON ν˜•μ‹μœΌλ‘œ 톡일
haeseonlee May 12, 2026
7f60e6e
refactor: data.sql μ˜ˆμ•½ λ‚ μ§œ μˆ˜μ •
haeseonlee May 12, 2026
c5f06d5
refactor: 였늘 λ‚ μ§œμ—μ„œ 이미 μ§€λ‚œ μ‹œκ°„ μ˜ˆμ•½ν•  수 없도둝 λ³€κ²½
haeseonlee May 12, 2026
5dab8e0
feat: λΈŒλΌμš°μ €μ—μ„œ μ—λŸ¬ λ°œμƒ μ‹œ μ‚¬μš©μžμ—κ²Œ λ©”μ„Έμ§€ ν‘œμ‹œ
haeseonlee May 12, 2026
2e29a42
docs: μ˜ˆμ•½ API λͺ…μ„Έμ„œμ— μ˜ˆμ•½ λ‚ μ§œ 및 μ‹œκ°„ λ³€κ²½ API μΆ”κ°€
haeseonlee May 13, 2026
8f37ae8
feat: 본인의 μ˜ˆμ•½ λ‚ μ§œΒ·μ‹œκ°„ λ³€κ²½ API μΆ”κ°€
haeseonlee May 13, 2026
ec79b75
feat: μ˜ˆμ•½ λ³€κ²½ ν™”λ©΄ κ΅¬ν˜„ 및 μ—λŸ¬ μΌ€μ΄μŠ€ 처리
haeseonlee May 13, 2026
7d96a91
test: ReservationService μ˜ˆμ•½ λ³€κ²½ κ΄€λ ¨ ν…ŒμŠ€νŠΈ μΆ”κ°€
haeseonlee May 13, 2026
99ec61c
test: UserReservationTest μ˜ˆμ•½ λ‚ μ§œ 및 μ‹œκ°„ λ³€κ²½ ν…ŒμŠ€νŠΈ μΆ”κ°€
haeseonlee May 13, 2026
65f36bb
refactor: μ—λŸ¬ 응닡 μ½”λ“œλ₯Ό 도메인 의미 기반으둜 λ³€κ²½
haeseonlee May 13, 2026
703d55a
refactor: μ˜ˆμ•½ νŽ˜μ΄μ§€ μ•„μ΄μ½˜ μ •λ ¬ μˆ˜μ •
haeseonlee May 13, 2026
2d22b22
refactor: Request/Response 클래슀 domain νŒ¨ν‚€μ§€μ—μ„œ 뢄리
haeseonlee May 14, 2026
b33d94e
refactor: 생성 μ‹œκ°„ 검증 둜직 μΆ”κ°€
haeseonlee May 14, 2026
bc5338e
refactor: JsonFormat μ–΄λ…Έν…Œμ΄μ…˜μ„ JacksonConfig μ „μ—­ μ„€μ •μœΌλ‘œ 뢄리
haeseonlee May 14, 2026
49a76f4
feat: μ§€λ‚˜κ°„ μ‹œκ°„μ— λŒ€ν•œ 검증 둜직 μΆ”κ°€
haeseonlee May 14, 2026
5daea5b
test: μ§€λ‚˜κ°„ μ‹œκ°„μ— λŒ€ν•œ 검증 둜직 ν…ŒμŠ€νŠΈ
haeseonlee May 14, 2026
c944e16
refactor: μ—λŸ¬ 둜그 λͺ…ν™•ν•˜κ²Œ λ³€κ²½
haeseonlee May 14, 2026
0e393e6
feat: μ·¨μ†Œ μΌ€μ΄μŠ€μ— λŒ€ν•œ 검증 둜직 μΆ”κ°€
haeseonlee May 14, 2026
74230be
feat: 이미 μ§€λ‚œ μ‹œκ°„μ— λŒ€ν•œ μ˜ˆμ•½ μ—…λ°μ΄νŠΈ 검증 둜직 μΆ”κ°€
haeseonlee May 14, 2026
460f4ea
refactor: λ©”μ„œλ“œλͺ… 톡일성 있게 λ³€κ²½
haeseonlee May 17, 2026
cf8f788
refactor: 검증 λ©”μ„Έμ§€ ν˜•μ‹ λ³€κ²½
haeseonlee May 17, 2026
1f4a41d
refactor: ReferencedDataException λ©”μ„Έμ§€ μ‚¬μš©μž μΉœν™”μ μœΌλ‘œ λ³€κ²½
haeseonlee May 17, 2026
9fbc893
test: μ˜ˆμ•½ μ˜ˆμ™Έ 상황에 λŒ€ν•œ ν…ŒμŠ€νŠΈ μΆ”κ°€
haeseonlee May 17, 2026
5d9410a
test: 에외 κ΄€λ ¨ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 보완
haeseonlee May 17, 2026
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
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# μ˜ˆμ•½ λ³€κ²½/μ·¨μ†Œμ™€ μ—λŸ¬ 처리

## κΈ°λŠ₯ κ΅¬ν˜„ λͺ©λ‘

### 1단계 - μ„œλΉ„μŠ€ μ •μ±… 적용

- [x] μ§€λ‚˜κ°„ λ‚ μ§œμ— λŒ€ν•œ μ˜ˆμ•½ 생성 λΆˆκ°€
- [x] 같은 λ‚ μ§œ + μ‹œκ°„ + ν…Œλ§ˆμ— 이미 μ˜ˆμ•½μ΄ 있으면 쀑볡 μ˜ˆμ•½ κ±°λΆ€
- [x] μ˜ˆμ•½μ΄ μ‘΄μž¬ν•˜λŠ” μ‹œκ°„ μ‚­μ œ λΆˆκ°€ (DB μ™Έλž˜ν‚€ RESTRICT μ„€μ •)
- [x] μœ νš¨ν•˜μ§€ μ•Šμ€ μž…λ ₯κ°’ κ±°λΆ€ (빈 이름, null λ‚ μ§œ/μ‹œκ°„/ν…Œλ§ˆ)

### 2단계 - μ—λŸ¬ 응닡 섀계

- [x] `@RestControllerAdvice`둜 μ „μ—­ μ˜ˆμ™Έ 처리 μΆ”κ°€
- [x] 500 μ—λŸ¬κ°€ μ‚¬μš©μžμ—κ²Œ λ…ΈμΆœλ˜μ§€ μ•Šλ„λ‘ 처리
- [x] μ—λŸ¬ 응닡 본문을 JSON ν˜•μ‹μœΌλ‘œ 톡일
- [x] λΈŒλΌμš°μ €μ—μ„œ μ—λŸ¬ λ°œμƒ μ‹œ μ‚¬μš©μžμ—κ²Œ 의미 μžˆλŠ” λ©”μ‹œμ§€ ν‘œμ‹œ

### 3단계 - λ‚΄ μ˜ˆμ•½ 쑰회/λ³€κ²½/μ·¨μ†Œ

- [x] μ΄λ¦„μœΌλ‘œ 본인의 μ˜ˆμ•½ λͺ©λ‘ 쑰회
- [x] 본인의 μ˜ˆμ•½ μ·¨μ†Œ
- [x] 본인의 μ˜ˆμ•½ λ‚ μ§œΒ·μ‹œκ°„ λ³€κ²½ API μΆ”κ°€
- [x] λ³€κ²½ ν™”λ©΄ κ΅¬ν˜„ (ν”„λ‘ νŠΈμ—”λ“œ)
- [x] λ³€κ²½Β·μ·¨μ†Œ μ—λŸ¬ μΌ€μ΄μŠ€ 처리 (이미 μ§€λ‚œ μ˜ˆμ•½, λ³€κ²½ν•˜λ €λŠ” μ‹œκ°„μ΄ 이미 μ˜ˆμ•½λœ 경우 λ“±)

---

## API λͺ…μ„Έ

### μ˜ˆμ•½

| λ©”μ„œλ“œ | URL | μ„€λͺ… | 성곡 응닡 |
|---|---|---------------|---|
| GET | /reservations | 전체 μ˜ˆμ•½ λͺ©λ‘ 쑰회 | 200 |
| GET | /reservations/mine?name={name} | μ΄λ¦„μœΌλ‘œ λ‚΄ μ˜ˆμ•½ 쑰회 | 200 |
| POST | /reservations | μ˜ˆμ•½ 생성 | 201 |
| DELETE | /reservations/{id} | μ˜ˆμ•½ μ·¨μ†Œ | 204 |
| PATCH | /reservations/{id} | μ˜ˆμ•½ λ‚ μ§œ 및 μ‹œκ°„ λ³€κ²½ | 200 |

### ν…Œλ§ˆ

| λ©”μ„œλ“œ | URL | μ„€λͺ… | 성곡 응닡 |
|---|---|---|---|
| GET | /themes | 전체 ν…Œλ§ˆ λͺ©λ‘ 쑰회 | 200 |
| GET | /themes/popular | 인기 ν…Œλ§ˆ μƒμœ„ 10개 쑰회 (졜근 1주일 κΈ°μ€€) | 200 |
| POST | /admin/themes | ν…Œλ§ˆ 생성 | 201 |
| DELETE | /admin/themes/{id} | ν…Œλ§ˆ μ‚­μ œ | 204 |

### μ‹œκ°„

| λ©”μ„œλ“œ | URL | μ„€λͺ… | 성곡 응닡 |
|---|---|---|---|
| GET | /times | 전체 μ˜ˆμ•½ μ‹œκ°„ λͺ©λ‘ 쑰회 | 200 |
| GET | /times/available?date={date}&themeId={themeId} | μ˜ˆμ•½ κ°€λŠ₯ν•œ μ‹œκ°„ 쑰회 | 200 |
| POST | /admin/times | μ˜ˆμ•½ μ‹œκ°„ 생성 | 201 |
| DELETE | /admin/times/{id} | μ˜ˆμ•½ μ‹œκ°„ μ‚­μ œ | 204 |

---

## μ—λŸ¬ 응닡 섀계

### 응닡 ν˜•μ‹

```json
{
"status": "INVALID_DATE",
"message": "이미 μ˜ˆμ•½λœ μ‹œκ°„μž…λ‹ˆλ‹€."
}
```

### μ—λŸ¬ μΌ€μ΄μŠ€

| 상황 | μƒνƒœμ½”λ“œ |
|---|---|
| 쀑볡 μ˜ˆμ•½ | 409 Conflict |
| μ§€λ‚˜κ°„ λ‚ μ§œ μ˜ˆμ•½ | 400 Bad Request |
| μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ¦¬μ†ŒμŠ€ | 404 Not Found |
| μ˜ˆμ•½μ΄ μ‘΄μž¬ν•˜λŠ” μ‹œκ°„/ν…Œλ§ˆ μ‚­μ œ | 400 Bad Request |
| μœ νš¨ν•˜μ§€ μ•Šμ€ μž…λ ₯κ°’ | 400 Bad Request |
23 changes: 23 additions & 0 deletions src/main/java/roomescape/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package roomescape.config;

import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.format.DateTimeFormatter;

@Configuration
public class JacksonConfig {

private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm");

@Bean
public Jackson2ObjectMapperBuilderCustomizer timeFormatCustomizer() {
return builder -> {
builder.serializers(new LocalTimeSerializer(TIME_FORMAT));
builder.deserializers(new LocalTimeDeserializer(TIME_FORMAT));
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
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.PatchMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import roomescape.domain.reservation.ReservationRequest;
import roomescape.domain.reservation.ReservationResponse;
import roomescape.dto.reservation.ReservationRequest;
import roomescape.dto.reservation.ReservationResponse;
import roomescape.service.ReservationService;

import java.net.URI;
Expand Down Expand Up @@ -47,6 +48,12 @@ public ResponseEntity<ReservationResponse> create(@RequestBody ReservationReques
return ResponseEntity.created(uri).body(newReservation);
}

@PatchMapping("/reservations/{id}")
public ResponseEntity<ReservationResponse> update(@PathVariable Long id, @RequestBody ReservationRequest reservationReq) {
ReservationResponse updatedReservation = reservationService.update(id, reservationReq);
return ResponseEntity.ok(updatedReservation);
}

@DeleteMapping("/reservations/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
reservationService.delete(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import roomescape.domain.reservationtime.ReservationTimeRequest;
import roomescape.domain.reservationtime.ReservationTimeResponse;
import roomescape.dto.reservationtime.ReservationTimeRequest;
import roomescape.dto.reservationtime.ReservationTimeResponse;
import roomescape.service.ReservationTimeService;

import java.net.URI;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/roomescape/controller/ThemeRestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.domain.theme.ThemeRequest;
import roomescape.domain.theme.ThemeResponse;
import roomescape.dto.theme.ThemeRequest;
import roomescape.dto.theme.ThemeResponse;
import roomescape.service.ThemeService;

import java.net.URI;
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/roomescape/domain/reservation/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@ public Reservation(Long id, String name, LocalDate date, ReservationTime time, T
this.createdAt = createdAt;
}

public Reservation reservationWithId(Long id) {
public Reservation withReservationId(Long id) {
return new Reservation(id, this.name, this.date, this.time, this.theme, this.createdAt);
}

public Reservation withUpdatedDateAndTime(LocalDate date, ReservationTime time) {
return new Reservation(id, this.name, date, time, this.theme, this.createdAt);
}
Comment thread
echo724 marked this conversation as resolved.

public Long getId() {
return id;
}
Expand Down

This file was deleted.

This file was deleted.

34 changes: 34 additions & 0 deletions src/main/java/roomescape/dto/reservation/ReservationRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package roomescape.dto.reservation;

import roomescape.exception.InvalidInputException;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public record ReservationRequest(String name, LocalDate date, Long timeId, Long themeId) {

public ReservationRequest {
List<String> emptyFields = new ArrayList<>();

if (name == null || name.isBlank()) {
emptyFields.add("name");
}

if (date == null) {
emptyFields.add("date");
}

if (timeId == null) {
emptyFields.add("timeId");
}

if (themeId == null) {
emptyFields.add("themeId");
}

if (!emptyFields.isEmpty()) {
throw new InvalidInputException("%s ν•„λ“œκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.".formatted(emptyFields));
}
Comment on lines +12 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

κ΅Ώ! κΉ”λ”ν•˜λ„€μš”!!

}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package roomescape.domain.reservation;
package roomescape.dto.reservation;

import roomescape.domain.reservationtime.ReservationTimeResponse;
import roomescape.domain.theme.ThemeResponse;
import roomescape.domain.reservation.Reservation;
import roomescape.dto.reservationtime.ReservationTimeResponse;
import roomescape.dto.theme.ThemeResponse;

import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package roomescape.dto.reservationtime;

import roomescape.exception.InvalidInputException;

import java.time.LocalTime;

public record ReservationTimeRequest(LocalTime startAt) {

public ReservationTimeRequest {
validateStartAt(startAt);
}

@Override
public LocalTime startAt() {
return startAt;
}

private static void validateStartAt(LocalTime startAt) {
if (startAt == null) {
throw new InvalidInputException("생성 μ‹œκ°„μ€ ν•„μˆ˜μž…λ‹ˆλ‹€.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package roomescape.domain.reservationtime;
package roomescape.dto.reservationtime;

import roomescape.domain.reservationtime.ReservationTime;

import java.time.LocalTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package roomescape.domain.theme;
package roomescape.dto.theme;

public record ThemeRequest(String name, String description, String url) {
Comment on lines +1 to 3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기도 검증이 ν•„μš”ν•˜μ§€ μ•Šμ„κΉŒμš”? null 값이 λ“€μ–΄μ˜€λŠ” κ²½μš°κ°€ μžˆμ„ 것 κ°™μ•„μš”!


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package roomescape.domain.theme;
package roomescape.dto.theme;

import roomescape.domain.theme.Theme;

public class ThemeResponse {

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/roomescape/exception/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.exception;

public record ErrorResponse(String errorCode, String message) {
}
52 changes: 52 additions & 0 deletions src/main/java/roomescape/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ReservationAlreadyExistException.class)
public ResponseEntity<ErrorResponse> handle(ReservationAlreadyExistException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("DUPLICATE_RESERVATION", e.getMessage()));
}

@ExceptionHandler(ReservationNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(ReservationNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("RESERVATION_NOT_FOUND", e.getMessage()));
}

@ExceptionHandler(ReservationTimeNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(ReservationTimeNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("TIME_NOT_FOUND", e.getMessage()));
}

@ExceptionHandler(ThemeNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(ThemeNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("THEME_NOT_FOUND", e.getMessage()));
}

@ExceptionHandler(InvalidReservationException.class)
public ResponseEntity<ErrorResponse> handle(InvalidReservationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("INVALID_DATE_OR_TIME", e.getMessage()));
}

@ExceptionHandler(ReferencedDataException.class)
public ResponseEntity<ErrorResponse> handle(ReferencedDataException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("DIFFERENCE_DATA_EXISTS", e.getMessage()));
}

@ExceptionHandler(InvalidInputException.class)
public ResponseEntity<ErrorResponse> handle(InvalidInputException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("INVALID_INPUT", e.getMessage()));
}
}
7 changes: 7 additions & 0 deletions src/main/java/roomescape/exception/InvalidInputException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class InvalidInputException extends IllegalArgumentException {
public InvalidInputException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package roomescape.exception;

public class InvalidReservationException extends RuntimeException {
public InvalidReservationException() {
super("이미 μ§€λ‚œ λ‚ μ§œμ΄κ±°λ‚˜ μ‹œκ°„μž…λ‹ˆλ‹€.");
}
}
Loading