Skip to content
Open
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
222 changes: 222 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,223 @@
# java-convenience-store-precourse

## 🚀 기능 요구 사항

구매자의 할인 혜택과 재고 상황을 고려하여 최종 결제 금액을 계산하고 안내하는 결제 시스템을 구현한다.

- 사용자가 입력한 상품의 가격과 수량을 기반으로 최종 결제 금액을 계산한다.
- 총구매액은 상품별 가격과 수량을 곱하여 계산하며, 프로모션 및 멤버십 할인 정책을 반영하여 최종 결제 금액을 산출한다.
- 구매 내역과 산출한 금액 정보를 영수증으로 출력한다.
- 영수증 출력 후 추가 구매를 진행할지 또는 종료할지를 선택할 수 있다.
- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
- `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다.

### 재고 관리

- 각 상품의 재고 수량을 고려하여 결제 가능 여부를 확인한다.
- 고객이 상품을 구매할 때마다, 결제된 수량만큼 해당 상품의 재고에서 차감하여 수량을 관리한다.
- 재고를 차감함으로써 시스템은 최신 재고 상태를 유지하며, 다음 고객이 구매할 때 정확한 재고 정보를 제공한다.

### 프로모션 할인

- 오늘 날짜가 프로모션 기간 내에 포함된 경우에만 할인을 적용한다.
- 프로모션은 N개 구매 시 1개 무료 증정(Buy N Get 1 Free)의 형태로 진행된다.
- 1+1 또는 2+1 프로모션이 각각 지정된 상품에 적용되며, 동일 상품에 여러 프로모션이 적용되지 않는다.
- 프로모션 혜택은 프로모션 재고 내에서만 적용할 수 있다.
- 프로모션 기간 중이라면 프로모션 재고를 우선적으로 차감하며, 프로모션 재고가 부족할 경우에는 일반 재고를 사용한다.
- 프로모션 적용이 가능한 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 필요한 수량을 추가로 가져오면 혜택을 받을 수 있음을 안내한다.
- 프로모션 재고가 부족하여 일부 수량을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제하게 됨을 안내한다.

### 멤버십 할인

- 멤버십 회원은 프로모션 미적용 금액의 30%를 할인받는다.
- 프로모션 적용 후 남은 금액에 대해 멤버십 할인을 적용한다.
- 멤버십 할인의 최대 한도는 8,000원이다.

### 영수증 출력

- 영수증은 고객의 구매 내역과 할인을 요약하여 출력한다.
- 영수증 항목은 아래와 같다.
- 구매 상품 내역: 구매한 상품명, 수량, 가격
- 증정 상품 내역: 프로모션에 따라 무료로 제공된 증정 상품의 목록
- 금액 정보
- 총구매액: 구매한 상품의 총 수량과 총 금액
- 행사할인: 프로모션에 의해 할인된 금액
- 멤버십할인: 멤버십에 의해 추가로 할인된 금액
- 내실돈: 최종 결제 금액
- 영수증의 구성 요소를 보기 좋게 정렬하여 고객이 쉽게 금액과 수량을 확인할 수 있게 한다.

### 입출력

- 입력 1
- 내용 : 구매할 상품과 수량
- 형식 : 상품([]), 수량(-), 열거(,)
- ex) [콜라-10],[사이다-3]
- 입력 2
- 내용 : 프로모션 상품 추가 여부, 정가 결제 여부, 멤버십 할인 적용 여부, 추가 구매 여부
- 형식 : Y / N
- 에러 메시지
- [ERROR]로 시작해야함
- ex) `[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.`

ex)

```java
안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 10개 탄산2+1
- 콜라 1,000원 10개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 5개
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[콜라-3],[에너지바-5]

멤버십 할인을 받으시겠습니까? (Y/N)
Y

===========W 편의점=============
상품명 수량 금액
콜라 3 3,000
에너지바 5 10,000
===========증 정=============
콜라 1
==============================
총구매액 8 13,000
행사할인 -1,000
멤버십할인 -3,000
내실돈 9,000

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
Y

안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 7개 탄산2+1
- 콜라 1,000원 10개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 재고 없음
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[콜라-10]

현재 콜라 4개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)
Y

멤버십 할인을 받으시겠습니까? (Y/N)
N

===========W 편의점=============
상품명 수량 금액
콜라 10 10,000
===========증 정=============
콜라 2
==============================
총구매액 10 10,000
행사할인 -2,000
멤버십할인 -0
내실돈 8,000

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
Y

안녕하세요. W편의점입니다.
현재 보유하고 있는 상품입니다.

- 콜라 1,000원 재고 없음 탄산2+1
- 콜라 1,000원 7개
- 사이다 1,000원 8개 탄산2+1
- 사이다 1,000원 7개
- 오렌지주스 1,800원 9개 MD추천상품
- 오렌지주스 1,800원 재고 없음
- 탄산수 1,200원 5개 탄산2+1
- 탄산수 1,200원 재고 없음
- 물 500원 10개
- 비타민워터 1,500원 6개
- 감자칩 1,500원 5개 반짝할인
- 감자칩 1,500원 5개
- 초코바 1,200원 5개 MD추천상품
- 초코바 1,200원 5개
- 에너지바 2,000원 재고 없음
- 정식도시락 6,400원 8개
- 컵라면 1,700원 1개 MD추천상품
- 컵라면 1,700원 10개

구매하실 상품명과 수량을 입력해 주세요. (예: [사이다-2],[감자칩-1])
[오렌지주스-1]

현재 오렌지주스은(는) 1개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)
Y

멤버십 할인을 받으시겠습니까? (Y/N)
Y

===========W 편의점=============
상품명 수량 금액
오렌지주스 2 3,600
===========증 정=============
오렌지주스 1
==============================
총구매액 2 3,600
행사할인 -1,800
멤버십할인 -0
내실돈 1,800

감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)
N

```

---

## 🎯 Java - 프로그래밍 요구 사항

- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.**
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다.
- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다.
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.**
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다.

### 라이브러리

- `camp.nextstep.edu.missionutils`에서 제공하는 API를 사용한다.
- 현재 날짜와 시간을 가져오려면 `camp.nextstep.edu.missionutils.DateTimes`의 `now()`를 활용한다.
- 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다.
7 changes: 6 additions & 1 deletion src/main/java/store/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package store;

import store.config.StoreConfig;
import store.controller.StoreController;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
StoreConfig config = new StoreConfig();
StoreController storeController = config.storeController();
storeController.run();
}
}
32 changes: 32 additions & 0 deletions src/main/java/store/config/StoreConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package store.config;

import store.controller.StoreController;
import store.service.*;
import store.view.InputView;
import store.view.OutputView;

public class StoreConfig {
public StoreController storeController() {
return new StoreController(productService(), promotionService(), checkoutService(), inputView(), outputView());
}

public ProductService productService() {
return new ProductService();
}

public PromotionService promotionService() {
return new PromotionService(productService(), inputView(), outputView()); // ✅ productService를 PromotionService에 주입
}

public CheckoutService checkoutService() {
return new CheckoutService();
}

public InputView inputView() {
return new InputView(new ShoppingCartService(productService()));
}

public OutputView outputView() {
return new OutputView();
}
}
46 changes: 46 additions & 0 deletions src/main/java/store/controller/StoreController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package store.controller;


import store.model.domain.Receipt;
import store.model.domain.ShoppingCart;
import store.service.CheckoutService;
import store.service.ProductService;
import store.service.PromotionService;
import store.view.InputView;
import store.view.OutputView;

public class StoreController {
private final ProductService productService;
private final PromotionService promotionService;
private final CheckoutService checkoutService;
private final InputView inputView;
private final OutputView outputView;

public StoreController(ProductService productService, PromotionService promotionService,
CheckoutService checkoutService, InputView inputView, OutputView outputView) {
this.productService = productService;
this.promotionService = promotionService;
this.checkoutService = checkoutService;
this.inputView = inputView;
this.outputView = outputView;
}

public void run() {
boolean continueShopping = true;

while (continueShopping) {
outputView.displayWelcomeMessage();
productService.displayProducts();

ShoppingCart cart = inputView.getUserShoppingCart();
promotionService.applyPromotions(cart);

boolean isMembership = inputView.askMembership();
Receipt receipt = checkoutService.checkout(cart, isMembership);

outputView.displayReceipt(receipt);

continueShopping = inputView.askContinueShopping();
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/store/enums/PromotionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package store.enums;

import java.util.Arrays;

public enum PromotionType {
BUY_TWO_GET_ONE("탄산2+1"),
MD_RECOMMENDED("MD추천상품"),
FLASH_DISCOUNT("반짝할인"),
NONE("null");

private final String label;

PromotionType(String label) {
this.label = label;
}

public String getLabel() {
return label;
}

public static PromotionType of(String label) {
return Arrays.stream(values())
.filter(type -> type.label.equals(label))
.findFirst()
.orElse(NONE);
}
}
7 changes: 7 additions & 0 deletions src/main/java/store/exception/InvalidInputException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package store.exception;

public class InvalidInputException extends IllegalArgumentException {
public InvalidInputException(String message) {
super(message);
}
}
Loading