# πͺ 맀μ₯ μμ½ λ° λ¦¬λ·° κ΄λ¦¬ μμ€ν
## π νλ‘μ νΈ μκ°
μ΄ νλ‘μ νΈλ 맀μ₯ μμ½ λ° λ¦¬λ·° κ΄λ¦¬λ₯Ό μν REST API μλΉμ€μ
λλ€.
### ν΅μ¬ κΈ°λ₯
- ννΈλ(μ μ₯): 맀μ₯ λ±λ‘/κ΄λ¦¬ λ° μμ½ μΉμΈ/κ±°μ
- μΌλ° μ¬μ©μ: 맀μ₯ κ²μ, μμ½, 체ν¬μΈ, 리뷰 μμ±
---
## βοΈ κ°λ° νκ²½
- μΈμ΄: Java 17
- νλ μμν¬: Spring Boot 3.3.7
- λ°μ΄ν°λ² μ΄μ€: MySQL 8.0
- λΉλ λꡬ: Gradle
- ν
μ€νΈ: JUnit5, Mockito
- κΈ°ν λΌμ΄λΈλ¬λ¦¬
- Spring Data JPA
- QueryDSL (λμ 쿼리 μ²λ¦¬)
- JWT (μΈμ¦/μΈκ°)
- Lombok (μ½λ κ°μν)
---
## π μ£Όμ κΈ°λ₯
### 1οΈβ£ νμκ°μ
λ° λ§€μ₯ λ±λ‘
- νμ κΆν κ΄λ¦¬
- USER: μΌλ° μ¬μ©μ
- PARTNER: 맀μ₯ κ΄λ¦¬μ(μ μ₯)
- JWT κΈ°λ° μΈμ¦
- Access Token λ°κΈ/κ²μ¦
- κΆνλ³ API μ κ·Ό μ μ΄
- 맀μ₯ κ΄λ¦¬ (PARTNER μ μ©)
- 맀μ₯ λ±λ‘/μμ /μμ
- 맀μ₯ μ 보: μ΄λ¦, μμΉ, μ€λͺ
, μ΄μμκ° λ±
### 2οΈβ£ 맀μ₯ κ²μ λ° μμ½
- 맀μ₯ κ²μ κΈ°λ₯
- ν€μλ κ²μ (맀μ₯λͺ
, μμΉ)
- μ λ ¬ μ΅μ
- κ°λλ€μ β
- λ³μ μ β
(νκ· λ³μ κΈ°μ€)
- 거리μ β
(νμ¬ μμΉ κΈ°μ€)
- λμ κ²μ 쑰건 (QueryDSL)
- νμ΄μ§ μ²λ¦¬
- μμ½ μμ€ν
- μμ½ μμ± β μ μ₯ μΉμΈ νμ
- μμ½ μν κ΄λ¦¬
- PENDING: μΉμΈ λκΈ°
- APPROVED: μΉμΈλ¨
- REJECTED: κ±°μ λ¨
- CHECKED_IN: 체ν¬μΈ μλ£
- COMPLETED: μ΄μ© μλ£
### 맀μ₯ κ²μ API μμ
- μ λ ¬ μ΅μ
- `sort=name,asc`: 맀μ₯λͺ
μ€λ¦μ°¨μ
- `sort=rating,desc`: λ³μ λμμ
- `sort=distance,asc`: κ°κΉμ΄ μ (μμΉ μ 보 νμ)
- `latitude`: νμ¬ μλ
- `longitude`: νμ¬ κ²½λ
### 3οΈβ£ 체ν¬μΈ λ° λ¦¬λ·°
- 체ν¬μΈ μμ€ν
- μμ½ μκ° 10λΆ μ λΆν° 체ν¬μΈ κ°λ₯
- ν€μ€μ€ν¬ μΈμ¦ μ½λ κ²μ¦
- 리뷰 μμ€ν
- μ΄μ© μλ£ ν 리뷰 μμ± κ°λ₯
- λ³μ λ° ν
μ€νΈ 리뷰
- κΆν κ΄λ¦¬
- μμ : μμ±μλ§ κ°λ₯
- μμ : μμ±μ λλ 맀μ₯ κ΄λ¦¬μ
---
## π API λͺ
μΈ
### νμ API
- νμκ°μ
: `POST /api/members/signup`
- Request: μ΄λ©μΌ, λΉλ°λ²νΈ, μ΄λ¦, μν (USER/PARTNER)
- λ‘κ·ΈμΈ: `POST /api/members/login`
- Response: JWT ν ν°
### 맀μ₯ API
- 맀μ₯ λ±λ‘: `POST /api/stores`
- 맀μ₯ λͺ©λ‘ μ‘°ν: `GET /api/stores`
- Query Parameters:
- keyword: κ²μμ΄
- sort: μ λ ¬ κΈ°μ€
- page: νμ΄μ§ λ²νΈ
- size: νμ΄μ§ ν¬κΈ°
- 맀μ₯ μμΈ μ‘°ν: `GET /api/stores/{id}`
### μμ½ API
- μμ½ μμ±: `POST /api/reservations`
- μμ½ μΉμΈ/κ±°μ : `PATCH /api/reservations/{id}`
- 체ν¬μΈ: `POST /api/reservations/check-in`
### 리뷰 API
- 리뷰 μμ±: `POST /api/reviews`
- 리뷰 μμ : `PATCH /api/reviews/{id}`
- 리뷰 μμ : `DELETE /api/reviews/{id}`
- 맀μ₯λ³ λ¦¬λ·° μ‘°ν: `GET /api/reviews/stores/{id}`
---
## ποΈ νλ‘μ νΈ κ΅¬μ‘°
```plaintext
src
βββ main
β βββ java
β β βββ com.zerobase.zbpaymentstudy
β β βββ common // κ³΅ν΅ κΈ°λ₯ (μ: μλ΅ νμ, μμΈ μ²λ¦¬)
β β βββ config // μ€μ νμΌ (μ: JWT, Security)
β β βββ domain // λλ©μΈλ³ κΈ°λ₯ (νμ, 맀μ₯, μμ½, 리뷰)
β β β βββ member // νμ κ΄λ¦¬
β β β βββ reservation // μμ½ κ΄λ¦¬
β β β βββ review // 리뷰 κ΄λ¦¬
β β β βββ store // 맀μ₯ κ΄λ¦¬
β β βββ exception // 컀μ€ν
μμΈ μ²λ¦¬
β βββ resources // μ€μ νμΌ λ° λ¦¬μμ€
βββ test // ν
μ€νΈ μ½λ
μμ€ν μ λ€μκ³Ό κ°μ κ³μΈ΅ κ΅¬μ‘°λ‘ μ€κ³λμμ΅λλ€.
- Presentation Layer: REST API μλν¬μΈνΈ μ 곡 λ° μμ²/μλ΅ μ²λ¦¬
- Business Layer: ν΅μ¬ λΉμ¦λμ€ λ‘μ§ λ° νΈλμμ κ΄λ¦¬
- Persistence Layer: λ°μ΄ν°λ² μ΄μ€ μ°μ° λ° λ°μ΄ν° μ κ·Ό
- Domain Layer: λΉμ¦λμ€ μν°ν° λ° κ·μΉ μ μ
μμ½λΆν° 리뷰κΉμ§μ μ 체 νλ‘μΈμ€λ₯Ό μκ°ννμ¬ νννμμ΅λλ€.
- Member: μ¬μ©μ μ 보 κ΄λ¦¬ (μΌλ° μ¬μ©μ/ννΈλ)
- Store: 맀μ₯ μ 보 κ΄λ¦¬
- Reservation: μμ½ μ 보 λ° μν κ΄λ¦¬
- Review: 리뷰 μ 보 κ΄λ¦¬
- Member(1) - Store(N): ννΈλλ μ¬λ¬ 맀μ₯μ μμ ν μ μμ
- Store(1) - Reservation(N): 맀μ₯μ μ¬λ¬ μμ½μ κ°μ§ μ μμ
- Reservation(1) - Review(1): νλμ μμ½λΉ νλμ 리뷰 μμ± κ°λ₯
- Java 17
- MySQL 8.0
-
μ μ₯μ ν΄λ‘
git clone https://github.com/your-username/store-reservation.git
-
λ°μ΄ν°λ² μ΄μ€ μ€μ
application.yml
νμΌμ λ°μ΄ν°λ² μ΄μ€ μ 보λ₯Ό μ λ ₯ν©λλ€.
spring: datasource: url: jdbc:mysql://localhost:3306/your_database username: your_username password: your_password
-
μ ν리μΌμ΄μ μ€ν
./gradlew bootRun
ν μ€νΈλ₯Ό μ€ννλ €λ©΄ λ€μ λͺ λ Ήμ΄λ₯Ό μ¬μ©νμΈμ.
./gradlew test
μ΄ νλ‘μ νΈλ MIT λΌμ΄μ μ€ νμ λ°°ν¬λ©λλ€.
μμΈν λ΄μ©μ LICENSE νμΌμ μ°Έκ³ νμΈμ.
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ReservationServiceImpl implements ReservationService {
private final ReservationRepository reservationRepository;
private final MemberRepository memberRepository;
private final StoreRepository storeRepository;
@Override
public ApiResponse<ReservationDto> createReservation(String memberEmail, ReservationCreateDto dto) {
try {
Member member = memberRepository.findByEmail(memberEmail)
.orElseThrow(() -> new BusinessException(ErrorCode.MEMBER_NOT_FOUND));
Store store = storeRepository.findById(dto.storeId())
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
validateReservationTime(dto.reservationTime());
Reservation reservation = Reservation.builder()
.store(store)
.member(member)
.reservationTime(dto.reservationTime())
.status(ReservationStatus.PENDING)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
Reservation savedReservation = reservationRepository.save(reservation);
log.info("μμ½ μμ± μλ£ - memberEmail: {}, storeId: {}", memberEmail, dto.storeId());
return new ApiResponse<>("SUCCESS", "μμ½μ΄ μμ±λμμ΅λλ€.", ReservationDto.from(savedReservation));
} catch (BusinessException e) {
log.warn("μμ½ μμ± μ€ν¨ - {}", e.getMessage());
throw e;
} catch (Exception e) {
log.error("μμ½ μμ± μ€ μ€λ₯ λ°μ", e);
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
}
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ReviewServiceImpl implements ReviewService {
private final ReviewRepository reviewRepository;
private final ReservationRepository reservationRepository;
@Override
public ApiResponse<ReviewDto> createReview(String memberEmail, ReviewCreateDto dto) {
try {
Reservation reservation = reservationRepository.findById(dto.reservationId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESERVATION_NOT_FOUND));
validateReviewCreation(reservation, memberEmail);
Review review = Review.builder()
.reservation(reservation)
.rating(dto.rating())
.content(dto.content())
.createdAt(LocalDateTime.now())
.build();
Review savedReview = reviewRepository.save(review);
return new ApiResponse<>("SUCCESS", "λ¦¬λ·°κ° μμ±λμμ΅λλ€.", ReviewDto.from(savedReview));
} catch (BusinessException e) {
throw e;
}
}
}