diff --git a/README.md b/README.md index 0177d96..70951b5 100644 --- a/README.md +++ b/README.md @@ -1,275 +1,31 @@ -# 미션 - 크리스마스 프로모션 - -## 🔍 진행 방식 - -- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다. -- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. -- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. - -## 📮 미션 제출 방법 - -- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. - - GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://docs.google.com/document/d/1cmg0VpPkuvdaetxwp4hnyyFC_G-1f2Gr8nIDYIWcKC8/edit?usp=sharing) 문서를 참고해 - 제출한다. -- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다. - - 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고 - - **지원 플랫폼을 통해 과제를 제출하지 않으면 최종 제출되지 않은 것으로 처리되므로 주의한다.** - -## 🚨 과제 제출 전 체크 리스트 - 0점 방지 - -- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다. -- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다. -- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다. - -### 테스트 실행 가이드 - -- 터미널에서 `java -version`을 실행하여 Java 버전이 17인지 확인한다. - Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다. -- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고, - Windows 사용자의 경우 `gradlew.bat clean test` 또는 `./gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다. - -``` -BUILD SUCCESSFUL in 0s -``` - ---- - -## 🚀 기능 요구 사항 - -> 이번 미션은 이메일 형식의 기능 요구 사항입니다. 문제를 구현하는 데 필요한 요구사항과 배경지식은 이메일 내용에 전부 담겨있으니, 꼼꼼하게 확인하고 필요하다면 주어진 문제의 내용을 통해 유추하고 스스로 판단해 -> 구현해 주시면 됩니다. 문제의 모든 내용은 충분히 검토되었으며, 출제 의도를 담은 내용임을 알려드립니다. - -보낸 사람: 비즈니스팀 \<`biz@woowacourse.io`\> -받는 사람: 개발팀 \<`dev@woowacourse.io`\> - -제목: 12월 이벤트를 위한 개발 요청 - -안녕하세요. 비즈니스팀입니다! - -다가오는 2023년 12월에 우테코 식당에서 1년 중 제일 큰 이벤트를 개최하려고 합니다. -12월을 위해 이벤트 예산을 넉넉히 확보해 두었으니, 예산은 걱정하지 마세요~ - -특별히 이번 12월 이벤트를 진행하기 위해서, 개발팀의 도움이 많이 필요합니다. -아래 메뉴와 달력 이미지를 보면서 12월 이벤트 계획과 요청 내용을 본격적으로 설명해 드릴게요. - -#### 메뉴 - -``` -<애피타이저> -양송이수프(6,000), 타파스(5,500), 시저샐러드(8,000) - -<메인> -티본스테이크(55,000), 바비큐립(54,000), 해산물파스타(35,000), 크리스마스파스타(25,000) - -<디저트> -초코케이크(15,000), 아이스크림(5,000) - -<음료> -제로콜라(3,000), 레드와인(60,000), 샴페인(25,000) -``` - -#### 달력 - -![](image.png) - -#### 이벤트 목표 - -1. 중복된 할인과 증정을 허용해서, 고객들이 혜택을 많이 받는다는 것을 체감할 수 있게 하는 것 -2. 올해 12월에 지난 5년 중 최고의 판매 금액을 달성 -3. 12월 이벤트 참여 고객의 5%가 내년 1월 새해 이벤트에 재참여하는 것 - -#### 12월 이벤트 계획 - -- 크리스마스 디데이 할인 - - 이벤트 기간: 2023.12.1 ~ 2023.12.25 - - 1,000원으로 시작하여 크리스마스가 다가올수록 날마다 할인 금액이 100원씩 증가 - - 총주문 금액에서 해당 금액만큼 할인 - (e.g. 시작일인 12월 1일에 1,000원, 2일에 1,100원, ..., 25일엔 3,400원 할인) -- 평일 할인(일요일~목요일): 평일에는 디저트 메뉴를 메뉴 1개당 2,023원 할인 -- 주말 할인(금요일, 토요일): 주말에는 메인 메뉴를 메뉴 1개당 2,023원 할인 -- 특별 할인: 이벤트 달력에 별이 있으면 총주문 금액에서 1,000원 할인 -- 증정 이벤트: 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인 1개 증정 -- 이벤트 기간: '크리스마스 디데이 할인'을 제외한 다른 이벤트는 2023.12.1 ~ 2023.12.31 동안 적용 - -#### 혜택 금액에 따른 12월 이벤트 배지 부여 - -- 총혜택 금액에 따라 다른 이벤트 배지를 부여합니다. 이 배지는 2024 새해 이벤트에서 활용할 예정입니다. - 배지에 따라 새해 이벤트 참여 시, 각각 다른 새해 선물을 증정할 예정입니다. - - 5천 원 이상: 별 - - 1만 원 이상: 트리 - - 2만 원 이상: 산타 - -#### 고객에게 안내할 이벤트 주의 사항 - -- 총주문 금액 10,000원 이상부터 이벤트가 적용됩니다. -- 음료만 주문 시, 주문할 수 없습니다. -- 메뉴는 한 번에 최대 20개까지만 주문할 수 있습니다. - (e.g. 시저샐러드-1, 티본스테이크-1, 크리스마스파스타-1, 제로콜라-3, 아이스크림-1의 총개수는 7개) - -#### '12월 이벤트 플래너' 개발 요청 사항 - -- 고객들이 식당에 방문할 날짜와 메뉴를 미리 선택하면 이벤트 플래너가 주문 메뉴, 할인 전 총주문 금액, 증정 메뉴, 혜택 내역, 총혜택 금액, 할인 후 예상 결제 금액, 12월 이벤트 배지 내용을 보여주기를 기대합니다. -- 12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!) - - 방문할 날짜는 1 이상 31 이하의 숫자로만 입력받아 주세요. - - 1 이상 31 이하의 숫자가 아닌 경우, "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요. - - 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요. -- 주문하실 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1) - - 고객이 메뉴판에 없는 메뉴를 입력하는 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요. - - 메뉴의 개수는 1 이상의 숫자만 입력되도록 해주세요. 이외의 입력값은 "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요. - - 메뉴 형식이 예시와 다른 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요. - - 중복 메뉴를 입력한 경우(e.g. 시저샐러드-1,시저샐러드-1), "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요. - - 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요. -- 주문 메뉴의 출력 순서는 자유롭게 출력해 주세요. -- 총혜택 금액에 따라 이벤트 배지의 이름을 다르게 보여 주세요. -- 총혜택 금액 = 할인 금액의 합계 + 증정 메뉴의 가격 -- 할인 후 예상 결제 금액 = 할인 전 총주문 금액 - 할인 금액 -- 증정 메뉴 - - 증정 이벤트에 해당하지 않는 경우, 증정 메뉴 "없음"으로 보여 주세요. -- 혜택 내역 - - 고객에게 적용된 이벤트 내역만 보여 주세요. - - 적용된 이벤트가 하나도 없다면 혜택 내역 "없음"으로 보여 주세요. - - 혜택 내역에 여러 개의 이벤트가 적용된 경우, 출력 순서는 자유롭게 출력해주세요. -- 이벤트 배지 - - 이벤트 배지가 부여되지 않는 경우, "없음"으로 보여 주세요. -- 적용된 이벤트가 하나도 없는 경우는 아래 예시를 참고해 주세요. - -``` -안녕하세요! 우테코 식당 12월 이벤트 플래너입니다. -12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!) -26 -주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1) -타파스-1,제로콜라-1 -12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! - -<주문 메뉴> -타파스 1개 -제로콜라 1개 - -<할인 전 총주문 금액> -8,500원 - -<증정 메뉴> -없음 - -<혜택 내역> -없음 - -<총혜택 금액> -0원 - -<할인 후 예상 결제 금액> -8,500원 - -<12월 이벤트 배지> -없음 -``` - -#### 기대하는 '12월 이벤트 플래너'의 예시 모습 - -``` -안녕하세요! 우테코 식당 12월 이벤트 플래너입니다. -12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!) -3 -주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1) -티본스테이크-1,바비큐립-1,초코케이크-2,제로콜라-1 -12월 3일에 우테코 식당에서 받을 이벤트 혜택 미리 보기! - -<주문 메뉴> -티본스테이크 1개 -바비큐립 1개 -초코케이크 2개 -제로콜라 1개 - -<할인 전 총주문 금액> -142,000원 - -<증정 메뉴> -샴페인 1개 - -<혜택 내역> -크리스마스 디데이 할인: -1,200원 -평일 할인: -4,046원 -특별 할인: -1,000원 -증정 이벤트: -25,000원 - -<총혜택 금액> --31,246원 - -<할인 후 예상 결제 금액> -135,754원 - -<12월 이벤트 배지> -산타 -``` - -기대하는 예시를 한 개만 들어서 설명했지만, 더 다양한 사례가 있을 것으로 예상됩니다. -개발이 완료되는 대로 공유해 주시면, 비즈니스팀에서 1주일간 테스트를 진행하고 오픈할 예정입니다. -1주일 뒤에 예정된 '12월 이벤트 플래너' 개발 회의에서 더 자세한 얘기를 해보면 좋겠습니다. - -감사합니다. :) - ---- - -## 🎯 프로그래밍 요구 사항 - -- 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항 연산자를 쓰지 않는다. -- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. - - 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. -- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다. -- else 예약어를 쓰지 않는다. - - 힌트: if 조건절에서 값을 return 하는 방식으로 구현하면 else를 사용하지 않아도 된다. - - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. -- 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다. - - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다. -- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. - - `Exception`이 아닌 `IllegalArgumentException`, `IllegalStateException` 등과 같은 명확한 유형을 처리한다. - -### 추가된 요구 사항 - -- 아래 있는 `InputView`, `OutputView` 클래스를 참고하여 입출력 클래스를 구현한다. - - 입력과 출력을 담당하는 클래스를 별도로 구현한다. - - 해당 클래스의 패키지, 클래스명, 메서드의 반환 타입과 시그니처는 자유롭게 구현할 수 있다. - ```java - public class InputView { - public int readDate() { - System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)"); - String input = Console.readLine(); - // ... - } - // ... - } - ``` - ```java - public class OutputView { - public void printMenu() { - System.out.println("<주문 메뉴>"); - // ... - } - // ... - } - ``` - -### 라이브러리 - -- `camp.nextstep.edu.missionutils`에서 제공하는 `Console` API를 사용하여 구현해야 한다. - - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다. - ---- - -## ✏️ 과제 진행 요구 사항 - -- 미션은 [java-christmas-6](https://github.com/woowacourse-precourse/java-christmas-6) 저장소를 비공개 저장소로 생성해 시작한다. -- **기능을 구현하기 전 `docs/README.md`에 구현할 기능 목록을 정리**해 추가한다. -- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다. - - [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다. -- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://docs.google.com/document/d/1cmg0VpPkuvdaetxwp4hnyyFC_G-1f2Gr8nIDYIWcKC8/edit?usp=sharing) 문서를 참고한다. +## 기능 목록 + +1. **입력 검증** + - [x] **날짜 입력 유효성 검사**: 방문 날짜가 1 이상 31 이하인지 확인하고 유효하지 않을 경우 에러 메시지 출력. + - [x] **메뉴 및 개수 입력 유효성 검사**: 메뉴명과 형식, 개수가 유효한지 검사하고, 중복 메뉴 확인 후 유효하지 않은 경우 에러 메시지 출력. + - [x] **음료만 주문 시 에러 처리**: 음료만 주문했을 경우 에러 메시지 출력. + - [x] **총 주문 개수 제한 확인**: 한 번에 주문할 수 있는 메뉴 개수가 20개 이하인지 확인. +2. **입력 Handling** + - [x] **방문 날짜 입력 받기**: 1에서 31 사이의 숫자를 입력받아 유효한 날짜인지 확인. + - [x] **메뉴와 개수 입력 받기**: 사용자가 입력한 주문 내역을 파싱하고 유효성 검사. +3. **할인 계산** + - [x] **크리스마스 디데이 할인 계산**: 12월 1일부터 25일까지 날마다 100원씩 증가하는 할인 금액 적용. + - [x] **평일 할인 계산**: 평일(일요일~목요일)에 디저트 메뉴에 대해 각 메뉴당 2,023원 할인 적용. + - [x] **주말 할인 계산**: 주말(금요일, 토요일)에 메인 메뉴에 대해 각 메뉴당 2,023원 할인 적용. + - [x] **특별 할인 계산**: 달력에 별이 있는 날에는 총주문 금액에서 1,000원 할인 적용. + - [x] **증정 이벤트 계산**: 할인 전 총주문 금액이 12만 원 이상일 경우 샴페인 1개 증정. +4. **배지 계산** + - [x] **총 혜택 금액 계산**: 할인 금액의 합계와 증정 메뉴 가격을 합산하여 총 혜택 금액 계산. + - [x] **이벤트 배지 부여**: 총 혜택 금액에 따라 "별", "트리", "산타" 배지 부여. +5. **출력 Handling** + - [x] **주문 메뉴 출력**: 입력받은 주문 메뉴와 개수를 출력. + - [x] **할인 전 총주문 금액 출력**: 주문한 모든 메뉴의 총 금액을 출력. + - [x] **증정 메뉴 출력**: 증정 메뉴가 있는 경우 샴페인 출력, 없으면 "없음" 출력. + - [x] **혜택 내역 출력**: 적용된 할인 내역을 출력. 혜택이 없을 경우 "없음" 출력. + - [x] **총 혜택 금액 출력**: 할인 금액의 합계를 출력. + - [x] **할인 후 예상 결제 금액 출력**: 할인 전 총주문 금액에서 총 할인 금액을 뺀 결과를 출력. + - [x] **이벤트 배지 출력**: 부여된 이벤트 배지 출력. 배지가 없는 경우 "없음" 출력. +6. **오류 Handling** + - [x] **날짜 입력 에러 처리**: 유효하지 않은 날짜 입력 시 "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요." 출력. + - [x] **주문 입력 에러 처리**: 잘못된 메뉴나 형식 입력 시 "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 출력. + - [x] **IllegalArgumentException 발생 시 처리**: 입력된 값이 유효하지 않을 경우 예외 발생 및 에러 메시지 출력 후 입력 재시도 \ No newline at end of file diff --git a/src/main/java/christmas/Application.java b/src/main/java/christmas/Application.java index b9ba6a2..d1911fd 100644 --- a/src/main/java/christmas/Application.java +++ b/src/main/java/christmas/Application.java @@ -1,7 +1,12 @@ package christmas; +import christmas.config.AppConfig; +import christmas.controller.EventPlannerController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + AppConfig appConfig = new AppConfig(); + EventPlannerController controller = appConfig.eventPlannerController(); + controller.run(); } } diff --git a/src/main/java/christmas/config/AppConfig.java b/src/main/java/christmas/config/AppConfig.java new file mode 100644 index 0000000..bcd90ec --- /dev/null +++ b/src/main/java/christmas/config/AppConfig.java @@ -0,0 +1,49 @@ +package christmas.config; + +import christmas.controller.EventPlannerController; +import christmas.service.*; +import christmas.service.SpecialDayDiscount; +import christmas.service.WeekdayDessertDiscount; +import christmas.service.WeekendMainDiscount; +import christmas.service.discountpolicy.ChristmasCountdownDiscount; +import christmas.service.discountpolicy.DiscountPolicy; +import christmas.view.InputView; +import christmas.view.OutputView; + +import java.util.List; + +public class AppConfig { + + public EventPlannerController eventPlannerController() { + return new EventPlannerController(eventService(), inputView(), outputView()); + } + + public EventService eventService() { + return new EventService(discountService(), giftService()); + } + + public DiscountService discountService() { + return new DiscountService(discountPolicies()); + } + + public List discountPolicies() { + return List.of( + new ChristmasCountdownDiscount(), + new WeekdayDessertDiscount(), + new WeekendMainDiscount(), + new SpecialDayDiscount() + ); + } + + public GiftService giftService() { + return new GiftService(); + } + + public InputView inputView() { + return new InputView(); + } + + public OutputView outputView() { + return new OutputView(); + } +} diff --git a/src/main/java/christmas/controller/EventPlannerController.java b/src/main/java/christmas/controller/EventPlannerController.java new file mode 100644 index 0000000..355ba82 --- /dev/null +++ b/src/main/java/christmas/controller/EventPlannerController.java @@ -0,0 +1,89 @@ +package christmas.controller; + +import christmas.domain.Menu; +import christmas.domain.Order; +import christmas.domain.OrderItem; +import christmas.domain.EventResult; +import christmas.service.EventService; +import christmas.view.InputView; +import christmas.view.OutputView; +import christmas.validator.InputValidator; + +import java.util.ArrayList; +import java.util.List; + +public class EventPlannerController { + + private final EventService eventService; + private final OutputView outputView; + private final InputView inputView; + + public EventPlannerController(EventService eventService, InputView inputView, OutputView outputView) { + this.eventService = eventService; + this.outputView = outputView; + this.inputView = inputView; + } + + public void run() { + + int date = readDateWithRetry(); + List orderItems = readOrderWithRetry(); + + Order order = new Order(orderItems); + EventResult eventResult = eventService.planEvent(order, date); + + outputView.printEventResult(eventResult); + } + + private int readDateWithRetry() { + while (true) { + try { + int date = InputValidator.validateDate(inputView.readDate()); + return date; + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private List readOrderWithRetry() { + while (true) { + try { + String input = inputView.readOrder(); + return parseOrder(input); + } catch (IllegalArgumentException e) { + outputView.printErrorMessage(e.getMessage()); + } + } + } + + private List parseOrder(String input) { + List orderItems = new ArrayList<>(); + String[] items = input.split(","); + + for (String item : items) { + String[] details = item.split("-"); + InputValidator.validateMenuFormat(details); + + String menuName = details[0].trim(); + String quantityStr = details[1].trim(); + + InputValidator.validateMenuName(menuName); + int quantity = parseQuantity(quantityStr); + + InputValidator.validateQuantity(quantity); + + orderItems.add(new OrderItem(Menu.fromName(menuName), quantity)); + } + + return orderItems; + } + + private int parseQuantity(String quantityStr) { + try { + return Integer.parseInt(quantityStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."); + } + } +} diff --git a/src/main/java/christmas/domain/Badge.java b/src/main/java/christmas/domain/Badge.java new file mode 100644 index 0000000..46329c1 --- /dev/null +++ b/src/main/java/christmas/domain/Badge.java @@ -0,0 +1,36 @@ +package christmas.domain; + +import java.math.BigDecimal; + +public enum Badge { + NONE("없음", BigDecimal.valueOf(0)), + STAR("별", BigDecimal.valueOf(5000)), + TREE("트리", BigDecimal.valueOf(10000)), + SANTA("산타", BigDecimal.valueOf(20000)); + + private final String displayName; + private final BigDecimal minimumDiscount; + + Badge(String displayName, BigDecimal minimumDiscount) { + this.displayName = displayName; + this.minimumDiscount = minimumDiscount; + } + + public String getDisplayName() { + return displayName; + } + + public BigDecimal getMinimumDiscount() { + return minimumDiscount; + } + + public static Badge getBadgeForDiscount(BigDecimal discount) { + Badge assignedBadge = NONE; + for (Badge badge : Badge.values()) { + if (discount.compareTo(badge.getMinimumDiscount()) >= 0) { + assignedBadge = badge; + } + } + return assignedBadge; + } +} diff --git a/src/main/java/christmas/domain/EventResult.java b/src/main/java/christmas/domain/EventResult.java new file mode 100644 index 0000000..bba4d96 --- /dev/null +++ b/src/main/java/christmas/domain/EventResult.java @@ -0,0 +1,119 @@ +package christmas.domain; + +import java.math.BigDecimal; +import java.util.Map; + +public class EventResult { + + private final Order order; + private final BigDecimal totalDiscount; + private final BigDecimal discountedPrice; + private final Menu gift; + private final Badge badge; + private final Map discountDetails; + + public EventResult(Order order, BigDecimal totalDiscount, BigDecimal discountedPrice, Menu gift, Badge badge, Map discountDetails) { + this.order = order; + this.totalDiscount = totalDiscount; + this.discountedPrice = discountedPrice; + this.gift = gift; + this.badge = badge; + this.discountDetails = discountDetails; + } + + public Order getOrder() { + return order; + } + + public BigDecimal getTotalDiscount() { + return totalDiscount; + } + + public BigDecimal getDiscountedPrice() { + return discountedPrice; + } + + public Menu getGift() { + return gift; + } + + public Badge getBadge() { + return badge; + } + + public Map getDiscountDetails() { + return discountDetails; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + + String lineSeparator = System.lineSeparator(); + + result.append("12월 이벤트 결과 미리 보기!").append(lineSeparator).append(lineSeparator); + + // 주문 메뉴 출력 + result.append("<주문 메뉴>").append(lineSeparator); + for (OrderItem item : order.getOrderItems()) { + result.append(item).append(lineSeparator); + } + + // 할인 전 총 주문 금액 출력 + result.append(lineSeparator).append("<할인 전 총주문 금액>").append(lineSeparator); + BigDecimal totalPriceBeforeDiscount = order.getTotalPriceBeforeDiscount(); + result.append(formatCurrency(totalPriceBeforeDiscount)).append("원").append(lineSeparator); + + // 증정 메뉴 출력 + result.append(lineSeparator).append("<증정 메뉴>").append(lineSeparator); + if (gift != null) { + result.append(gift.getName()).append(" 1개").append(lineSeparator); + } else { + result.append("없음").append(lineSeparator); + } + + // 혜택 내역 출력 + result.append(lineSeparator).append("<혜택 내역>").append(lineSeparator); + if (discountDetails.isEmpty() && gift == null) { + result.append("없음").append(lineSeparator); + } else { + for (Map.Entry entry : discountDetails.entrySet()) { + result.append(entry.getKey()).append(": -").append(formatCurrency(entry.getValue())).append("원").append(lineSeparator); + } + if (gift != null) { + result.append("증정 이벤트: -").append(formatCurrency(gift.getPrice())).append("원").append(lineSeparator); + } + } + + // 총 혜택 금액 출력 (0보다 큰 경우와 0인 경우 구분) + result.append(lineSeparator).append("<총혜택 금액>").append(lineSeparator); + BigDecimal totalBenefit = totalDiscount; + if (gift != null) { + totalBenefit = totalBenefit.add(gift.getPrice()); + } + if (totalBenefit.compareTo(BigDecimal.ZERO) > 0) { + result.append("-").append(formatCurrency(totalBenefit)).append("원").append(lineSeparator); + } else { + result.append("0원").append(lineSeparator); + } + + // 할인 후 예상 결제 금액 출력 + result.append(lineSeparator).append("<할인 후 예상 결제 금액>").append(lineSeparator); + result.append(formatCurrency(discountedPrice)).append("원").append(lineSeparator); + + // 이벤트 배지 출력 + result.append(lineSeparator).append("<12월 이벤트 배지>").append(lineSeparator); + if (badge != Badge.NONE) { + result.append(badge.getDisplayName()).append(lineSeparator); + } else { + result.append("없음").append(lineSeparator); + } + + return result.toString(); + } + + + private String formatCurrency(BigDecimal amount) { + return String.format("%,d", amount.longValue()); + } +} diff --git a/src/main/java/christmas/domain/Menu.java b/src/main/java/christmas/domain/Menu.java new file mode 100644 index 0000000..846cd68 --- /dev/null +++ b/src/main/java/christmas/domain/Menu.java @@ -0,0 +1,57 @@ +package christmas.domain; + +import java.math.BigDecimal; + +public enum Menu { + + MUSHROOM_SOUP("양송이수프", "애피타이저", BigDecimal.valueOf(6_000)), + TAPAS("타파스", "애피타이저", BigDecimal.valueOf(5_500)), + CAESAR_SALAD("시저샐러드", "애피타이저", BigDecimal.valueOf(8_000)), + + + T_BONE_STEAK("티본스테이크", "메인", BigDecimal.valueOf(55_000)), + BBQ_RIB("바비큐립", "메인", BigDecimal.valueOf(54_000)), + SEAFOOD_PASTA("해산물파스타", "메인", BigDecimal.valueOf(35_000)), + CHRISTMAS_PASTA("크리스마스파스타", "메인", BigDecimal.valueOf(25_000)), + + + CHOCOLATE_CAKE("초코케이크", "디저트", BigDecimal.valueOf(15_000)), + ICE_CREAM("아이스크림", "디저트", BigDecimal.valueOf(5_000)), + + + ZERO_COKE("제로콜라", "음료", BigDecimal.valueOf(3_000)), + RED_WINE("레드와인", "음료", BigDecimal.valueOf(60_000)), + CHAMPAGNE("샴페인", "음료", BigDecimal.valueOf(25_000)); + + private final String name; + private final String category; + private final BigDecimal price; + + Menu(String name, String category, BigDecimal price) { + this.name = name; + this.category = category; + this.price = price; + } + + public String getName() { + return name; + } + + public String getCategory() { + return category; + } + + public BigDecimal getPrice() { + return price; + } + + + public static Menu fromName(String name) { + for (Menu menu : values()) { + if (menu.name.equals(name)) { + return menu; + } + } + throw new IllegalArgumentException("[ERROR] 유효하지 않은 메뉴 이름입니다. 다시 입력해 주세요."); + } +} diff --git a/src/main/java/christmas/domain/Order.java b/src/main/java/christmas/domain/Order.java new file mode 100644 index 0000000..5121d60 --- /dev/null +++ b/src/main/java/christmas/domain/Order.java @@ -0,0 +1,48 @@ +package christmas.domain; + +import java.math.BigDecimal; +import java.util.List; + +public class Order { + + private final List orderItems; + + public Order(List orderItems) { + validateOrderItems(orderItems); + this.orderItems = orderItems; + } + + private void validateOrderItems(List orderItems) { + if (orderItems == null || orderItems.isEmpty()) { + throw new IllegalArgumentException("[ERROR] 주문 항목이 비어 있습니다."); + } + + if (orderItems.stream().anyMatch(item -> item.getMenu().getCategory().equals("음료") && orderItems.size() == 1)) { + throw new IllegalArgumentException("[ERROR] 음료만 주문할 수 없습니다."); + } + + int totalQuantity = orderItems.stream().mapToInt(OrderItem::getQuantity).sum(); + if (totalQuantity > 20) { + throw new IllegalArgumentException("[ERROR] 주문 수량은 20개 이하로 해주세요."); + } + } + + public BigDecimal getTotalPriceBeforeDiscount() { + return orderItems.stream() + .map(item -> item.getMenu().getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public List getOrderItems() { + return orderItems; + } + + @Override + public String toString() { + StringBuilder orderDetails = new StringBuilder("<주문 메뉴>\n"); + for (OrderItem item : orderItems) { + orderDetails.append(item.toString()).append("\n"); + } + return orderDetails.toString(); + } +} diff --git a/src/main/java/christmas/domain/OrderItem.java b/src/main/java/christmas/domain/OrderItem.java new file mode 100644 index 0000000..0e68529 --- /dev/null +++ b/src/main/java/christmas/domain/OrderItem.java @@ -0,0 +1,28 @@ +package christmas.domain; + +public class OrderItem { + + private final Menu menu; + private final int quantity; + + public OrderItem(Menu menu, int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("[ERROR] 수량은 1개 이상이어야 합니다."); + } + this.menu = menu; + this.quantity = quantity; + } + + public Menu getMenu() { + return menu; + } + + public int getQuantity() { + return quantity; + } + + @Override + public String toString() { + return String.format("%s %d개", menu.getName(), quantity); + } +} diff --git a/src/main/java/christmas/service/DiscountService.java b/src/main/java/christmas/service/DiscountService.java new file mode 100644 index 0000000..5250663 --- /dev/null +++ b/src/main/java/christmas/service/DiscountService.java @@ -0,0 +1,44 @@ +package christmas.service; + +import christmas.domain.Order; +import christmas.domain.OrderItem; +import christmas.service.discountpolicy.DiscountPolicy; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class DiscountService { + + private final List discountPolicies; + + public DiscountService(List discountPolicies) { + this.discountPolicies = discountPolicies; + } + + public BigDecimal calculateDiscount(Order order, int date) { + BigDecimal totalDiscount = BigDecimal.ZERO; + + for (DiscountPolicy policy : discountPolicies) { + BigDecimal discount = policy.applyDiscount(order, date); + totalDiscount = totalDiscount.add(discount); + } + + return totalDiscount; + } + + public Map calculateDiscountDetails(Order order, int date) { + + Map discountDetails = new LinkedHashMap<>(); + + for (DiscountPolicy policy : discountPolicies) { + BigDecimal discount = policy.applyDiscount(order, date); + if (discount.compareTo(BigDecimal.ZERO) > 0) { + discountDetails.put(policy.getPolicyName(), discount); + } + } + + return discountDetails; + } +} diff --git a/src/main/java/christmas/service/EventService.java b/src/main/java/christmas/service/EventService.java new file mode 100644 index 0000000..939a865 --- /dev/null +++ b/src/main/java/christmas/service/EventService.java @@ -0,0 +1,57 @@ +package christmas.service; + +import christmas.domain.Badge; +import christmas.domain.EventResult; +import christmas.domain.Menu; +import christmas.domain.Order; + +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; + +public class EventService { + + private final DiscountService discountService; + private final GiftService giftService; + + public EventService(DiscountService discountService, GiftService giftService) { + this.discountService = discountService; + this.giftService = giftService; + } + + public EventResult planEvent(Order order, int date) { + // 할인 계산 및 할인 내역 세부 정보 저장 + Map discountDetails = discountService.calculateDiscountDetails(order, date); + BigDecimal totalDiscount = discountDetails.values().stream() + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 증정 품목 확인 + Menu gift = giftService.checkGiftEligibility(order); + + // 최종 결제 금액 계산 + BigDecimal finalPrice = order.getTotalPriceBeforeDiscount().subtract(totalDiscount); + + // 배지 부여 + Badge badge = assignBadge(totalDiscount, gift); + + return new EventResult(order, totalDiscount, finalPrice, gift, badge, discountDetails); + } + + private Badge assignBadge(BigDecimal totalDiscount, Menu gift) { + BigDecimal totalBenefit = totalDiscount; + if (gift != null) { + totalBenefit = totalBenefit.add(gift.getPrice()); + } + + if (totalBenefit.compareTo(new BigDecimal("20000")) >= 0) { + return Badge.SANTA; + } + if (totalBenefit.compareTo(new BigDecimal("10000")) >= 0) { + return Badge.TREE; + } + if (totalBenefit.compareTo(new BigDecimal("5000")) >= 0) { + return Badge.STAR; + } + return Badge.NONE; + } +} diff --git a/src/main/java/christmas/service/GiftService.java b/src/main/java/christmas/service/GiftService.java new file mode 100644 index 0000000..667157d --- /dev/null +++ b/src/main/java/christmas/service/GiftService.java @@ -0,0 +1,16 @@ +package christmas.service; + +import christmas.domain.Menu; +import christmas.domain.Order; + +public class GiftService { + + private static final int GIFT_THRESHOLD = 120_000; + + public Menu checkGiftEligibility(Order order) { + if (order.getTotalPriceBeforeDiscount().compareTo(new java.math.BigDecimal(GIFT_THRESHOLD)) >= 0) { + return Menu.CHAMPAGNE; + } + return null; + } +} diff --git a/src/main/java/christmas/service/discountpolicy/ChristmasCountdownDiscount.java b/src/main/java/christmas/service/discountpolicy/ChristmasCountdownDiscount.java new file mode 100644 index 0000000..7979fbb --- /dev/null +++ b/src/main/java/christmas/service/discountpolicy/ChristmasCountdownDiscount.java @@ -0,0 +1,22 @@ +package christmas.service.discountpolicy; + +import christmas.domain.Order; + +import java.math.BigDecimal; + +public class ChristmasCountdownDiscount implements DiscountPolicy { + + @Override + public BigDecimal applyDiscount(Order order, int date) { + if (date >= 1 && date <= 25) { + BigDecimal discountAmount = BigDecimal.valueOf(1000 + (date - 1) * 100); + return discountAmount.min(order.getTotalPriceBeforeDiscount()); + } + return BigDecimal.ZERO; + } + + @Override + public String getPolicyName() { + return "크리스마스 디데이 할인"; + } +} diff --git a/src/main/java/christmas/service/discountpolicy/DiscountPolicy.java b/src/main/java/christmas/service/discountpolicy/DiscountPolicy.java new file mode 100644 index 0000000..2c9a9b0 --- /dev/null +++ b/src/main/java/christmas/service/discountpolicy/DiscountPolicy.java @@ -0,0 +1,11 @@ +package christmas.service.discountpolicy; + +import christmas.domain.Order; + +import java.math.BigDecimal; + +public interface DiscountPolicy { + BigDecimal applyDiscount(Order order, int date); + + String getPolicyName(); +} diff --git a/src/main/java/christmas/service/discountpolicy/SpecialDayDiscount.java b/src/main/java/christmas/service/discountpolicy/SpecialDayDiscount.java new file mode 100644 index 0000000..7fe77a3 --- /dev/null +++ b/src/main/java/christmas/service/discountpolicy/SpecialDayDiscount.java @@ -0,0 +1,29 @@ +package christmas.service; + +import christmas.domain.Order; +import christmas.service.discountpolicy.DiscountPolicy; + +import java.math.BigDecimal; +import java.util.Set; + +public class SpecialDayDiscount implements DiscountPolicy { + + private final Set specialDays; + + public SpecialDayDiscount() { + this.specialDays = Set.of(3, 10, 17, 24, 25, 31); + } + + @Override + public BigDecimal applyDiscount(Order order, int date) { + if (specialDays.contains(date)) { + return BigDecimal.valueOf(1000); + } + return BigDecimal.ZERO; + } + + @Override + public String getPolicyName() { + return "특별 할인"; + } +} diff --git a/src/main/java/christmas/service/discountpolicy/WeekdayDessertDiscount.java b/src/main/java/christmas/service/discountpolicy/WeekdayDessertDiscount.java new file mode 100644 index 0000000..46855e1 --- /dev/null +++ b/src/main/java/christmas/service/discountpolicy/WeekdayDessertDiscount.java @@ -0,0 +1,30 @@ +package christmas.service; + +import christmas.domain.Order; +import christmas.domain.OrderItem; +import christmas.service.discountpolicy.DiscountPolicy; +import christmas.util.DateUtils; + +import java.math.BigDecimal; + +public class WeekdayDessertDiscount implements DiscountPolicy { + + @Override + public BigDecimal applyDiscount(Order order, int date) { + if (DateUtils.isWeekday(date)) { + BigDecimal discount = BigDecimal.ZERO; + for (OrderItem item : order.getOrderItems()) { + if (item.getMenu().getCategory().equals("디저트")) { + discount = discount.add(BigDecimal.valueOf(2023).multiply(BigDecimal.valueOf(item.getQuantity()))); + } + } + return discount; + } + return BigDecimal.ZERO; + } + + @Override + public String getPolicyName() { + return "평일 할인"; + } +} diff --git a/src/main/java/christmas/service/discountpolicy/WeekendMainDiscount.java b/src/main/java/christmas/service/discountpolicy/WeekendMainDiscount.java new file mode 100644 index 0000000..2f6d487 --- /dev/null +++ b/src/main/java/christmas/service/discountpolicy/WeekendMainDiscount.java @@ -0,0 +1,31 @@ +package christmas.service; + +import christmas.domain.Order; +import christmas.domain.OrderItem; +import christmas.service.discountpolicy.DiscountPolicy; +import christmas.util.DateUtils; + +import java.math.BigDecimal; + +public class WeekendMainDiscount implements DiscountPolicy { + + @Override + public BigDecimal applyDiscount(Order order, int date) { + // 주말인지 확인 (금요일, 토요일) + if (DateUtils.isWeekend(date)) { + BigDecimal discount = BigDecimal.ZERO; + for (OrderItem item : order.getOrderItems()) { + if (item.getMenu().getCategory().equals("메인")) { + discount = discount.add(BigDecimal.valueOf(2023).multiply(BigDecimal.valueOf(item.getQuantity()))); + } + } + return discount; + } + return BigDecimal.ZERO; + } + + @Override + public String getPolicyName() { + return "주말할인"; + } +} diff --git a/src/main/java/christmas/util/DateUtils.java b/src/main/java/christmas/util/DateUtils.java new file mode 100644 index 0000000..cce272e --- /dev/null +++ b/src/main/java/christmas/util/DateUtils.java @@ -0,0 +1,25 @@ +package christmas.util; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +public class DateUtils { + + public static DayOfWeek getDayOfWeek(int dayOfDecember) { + if (dayOfDecember < 1 || dayOfDecember > 31) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 1에서 31 사이의 숫자를 입력해 주세요."); + } + LocalDate date = LocalDate.of(2023, 12, dayOfDecember); + return date.getDayOfWeek(); + } + + public static boolean isWeekday(int dayOfDecember) { + DayOfWeek dayOfWeek = getDayOfWeek(dayOfDecember); + return dayOfWeek != DayOfWeek.FRIDAY && dayOfWeek != DayOfWeek.SATURDAY; + } + + public static boolean isWeekend(int dayOfDecember) { + DayOfWeek dayOfWeek = getDayOfWeek(dayOfDecember); + return dayOfWeek == DayOfWeek.FRIDAY || dayOfWeek == DayOfWeek.SATURDAY; + } +} diff --git a/src/main/java/christmas/validator/InputValidator.java b/src/main/java/christmas/validator/InputValidator.java new file mode 100644 index 0000000..cfe4a96 --- /dev/null +++ b/src/main/java/christmas/validator/InputValidator.java @@ -0,0 +1,38 @@ +package christmas.validator; + +import christmas.domain.Menu; + +public class InputValidator { + + public static int validateDate(String input) { + try { + int date = Integer.parseInt(input); + if (date < 1 || date > 31) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."); + } + return date; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."); + } + } + + public static void validateMenuFormat(String[] details) { + if (details.length != 2) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 주문 형식입니다. 다시 입력해 주세요."); + } + } + + public static void validateQuantity(int quantity) { + if (quantity < 1) { + throw new IllegalArgumentException("[ERROR] 수량은 1개 이상이어야 합니다."); + } + } + + public static void validateMenuName(String menuName) { + try { + Menu.fromName(menuName); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("[ERROR] 유효하지 않은 메뉴입니다. 다시 입력해 주세요."); + } + } +} diff --git a/src/main/java/christmas/view/InputView.java b/src/main/java/christmas/view/InputView.java new file mode 100644 index 0000000..bc9c10d --- /dev/null +++ b/src/main/java/christmas/view/InputView.java @@ -0,0 +1,16 @@ +package christmas.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + public String readDate() { + System.out.println("안녕하세요! 우테코 식당 12월 이벤트 플래너입니다."); + System.out.println("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)"); + return Console.readLine(); + } + public String readOrder() { + System.out.println("주문하실 메뉴와 개수를 입력해 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)"); + return Console.readLine(); + } +} diff --git a/src/main/java/christmas/view/OutputView.java b/src/main/java/christmas/view/OutputView.java new file mode 100644 index 0000000..0efb0dd --- /dev/null +++ b/src/main/java/christmas/view/OutputView.java @@ -0,0 +1,24 @@ +package christmas.view; + +import christmas.domain.EventResult; +import christmas.domain.OrderItem; + +import java.util.List; + +public class OutputView { + + public void printEventResult(EventResult result) { + System.out.println(result); + } + + public void printOrderItems(List orderItems) { + System.out.println("<주문 메뉴>"); + for (OrderItem item : orderItems) { + System.out.println(item); + } + } + + public void printErrorMessage(String message) { + System.out.println(message); + } +}