diff --git a/README.md b/README.md index d54f268..c425af8 100644 --- a/README.md +++ b/README.md @@ -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()`을 ν™œμš©ν•œλ‹€. diff --git a/src/main/java/store/Application.java b/src/main/java/store/Application.java index ec4afd8..dbf6bf1 100644 --- a/src/main/java/store/Application.java +++ b/src/main/java/store/Application.java @@ -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(); } } diff --git a/src/main/java/store/config/StoreConfig.java b/src/main/java/store/config/StoreConfig.java new file mode 100644 index 0000000..3cc3213 --- /dev/null +++ b/src/main/java/store/config/StoreConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/store/controller/StoreController.java b/src/main/java/store/controller/StoreController.java new file mode 100644 index 0000000..ee6c16a --- /dev/null +++ b/src/main/java/store/controller/StoreController.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/store/enums/PromotionType.java b/src/main/java/store/enums/PromotionType.java new file mode 100644 index 0000000..2a37333 --- /dev/null +++ b/src/main/java/store/enums/PromotionType.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/store/exception/InvalidInputException.java b/src/main/java/store/exception/InvalidInputException.java new file mode 100644 index 0000000..047749c --- /dev/null +++ b/src/main/java/store/exception/InvalidInputException.java @@ -0,0 +1,7 @@ +package store.exception; + +public class InvalidInputException extends IllegalArgumentException { + public InvalidInputException(String message) { + super(message); + } +} diff --git a/src/main/java/store/model/domain/Product.java b/src/main/java/store/model/domain/Product.java new file mode 100644 index 0000000..23b7e58 --- /dev/null +++ b/src/main/java/store/model/domain/Product.java @@ -0,0 +1,73 @@ +package store.model.domain; + +import java.text.NumberFormat; +import java.util.Locale; + +public class Product { + private static final NumberFormat PRICE_FORMAT = NumberFormat.getInstance(Locale.KOREA); + + private final String name; + private final int price; + private int stock; + private int promotionStock; + private final Promotion promotion; + + public Product(String name, int price, int stock, Promotion promotion) { + this.name = name; + this.price = price; + this.stock = stock; + this.promotionStock = promotionStock; + this.promotion = promotion; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public int getStock() { + return stock; + } + + public int getPromotionStock() { + return promotionStock; + } + + public Promotion getPromotion() { + return promotion; + } + + /** + * βœ… 재고 κ°μ†Œ λ©”μ„œλ“œ + * - PromotionCalculatorμ—μ„œ ν”„λ‘œλͺ¨μ…˜ 적용 μ—¬λΆ€λ₯Ό νŒλ‹¨ν•œ ν›„ ν˜ΈμΆœν•΄μ•Ό 함. + */ + public void reduceStock(int quantity) { + if (quantity > stock) { + throw new IllegalArgumentException("[ERROR] 재고 λΆ€μ‘±"); + } + stock -= quantity; + } + + /** + * βœ… ν”„λ‘œλͺ¨μ…˜ 재고 κ°μ†Œ λ©”μ„œλ“œ + * - PromotionCalculatorμ—μ„œ ν”„λ‘œλͺ¨μ…˜μ„ ν™•μΈν•œ ν›„ ν˜ΈμΆœν•΄μ•Ό 함. + */ + public void reducePromotionStock(int quantity) { + if (quantity > promotionStock) { + throw new IllegalArgumentException("[ERROR] ν”„λ‘œλͺ¨μ…˜ 재고 λΆ€μ‘±"); + } + promotionStock -= quantity; + } + + @Override + public String toString() { + String formattedPrice = PRICE_FORMAT.format(price) + "원"; + String formattedStock = (stock > 0) ? stock + "개" : "재고 μ—†μŒ"; + String promoText = (promotion != null) ? promotion.getName() : ""; + + return "- " + name + " " + formattedPrice + " " + formattedStock + (promoText.isEmpty() ? "" : " " + promoText); + } +} \ No newline at end of file diff --git a/src/main/java/store/model/domain/Promotion.java b/src/main/java/store/model/domain/Promotion.java new file mode 100644 index 0000000..583a9b4 --- /dev/null +++ b/src/main/java/store/model/domain/Promotion.java @@ -0,0 +1,39 @@ +package store.model.domain; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class Promotion { + private final String name; + private final int buyCount; + private final int getCount; + private final LocalDate startDate; + private final LocalDate endDate; + + public Promotion(String name, int buyCount, int getCount, String startDate, String endDate) { + this.name = name; + this.buyCount = buyCount; + this.getCount = getCount; + this.startDate = LocalDate.parse(startDate, DateTimeFormatter.ISO_DATE); + this.endDate = LocalDate.parse(endDate, DateTimeFormatter.ISO_DATE); + } + + public String getName() { + return name; + } + + public int getBuyCount() { + return buyCount; + } + + public int getGetCount() { + return getCount; + } + + /** + * βœ… ν”„λ‘œλͺ¨μ…˜μ΄ ν˜„μž¬ λ‚ μ§œμ— μœ νš¨ν•œμ§€ 확인 + */ + public boolean isActive(LocalDate today) { + return !today.isBefore(startDate) && !today.isAfter(endDate); + } +} \ No newline at end of file diff --git a/src/main/java/store/model/domain/Receipt.java b/src/main/java/store/model/domain/Receipt.java new file mode 100644 index 0000000..2998cc3 --- /dev/null +++ b/src/main/java/store/model/domain/Receipt.java @@ -0,0 +1,47 @@ +package store.model.domain; + +import java.util.List; +import store.model.dto.FreeItem; +import store.model.dto.PurchaseItem; + +public class Receipt { + private final List purchasedItems; + private final List freeItems; + private final int totalAmount; + private final int promotionDiscount; + private final int membershipDiscount; + private final int finalAmount; + + public Receipt(List purchasedItems, List freeItems, int totalAmount, int promotionDiscount, int membershipDiscount, int finalAmount) { + this.purchasedItems = purchasedItems; + this.freeItems = freeItems; + this.totalAmount = totalAmount; + this.promotionDiscount = promotionDiscount; + this.membershipDiscount = membershipDiscount; + this.finalAmount = finalAmount; + } + + public List getPurchasedItems() { + return purchasedItems; + } + + public List getFreeItems() { + return freeItems; + } + + public int getTotalAmount() { + return totalAmount; + } + + public int getPromotionDiscount() { + return promotionDiscount; + } + + public int getMembershipDiscount() { + return membershipDiscount; + } + + public int getFinalAmount() { + return finalAmount; + } +} \ No newline at end of file diff --git a/src/main/java/store/model/domain/ShoppingCart.java b/src/main/java/store/model/domain/ShoppingCart.java new file mode 100644 index 0000000..cf5c355 --- /dev/null +++ b/src/main/java/store/model/domain/ShoppingCart.java @@ -0,0 +1,49 @@ +package store.model.domain; + +import java.util.HashMap; +import java.util.Map; + +public class ShoppingCart { + private final Map items; + + public ShoppingCart() { + this.items = new HashMap<>(); + } + + public void addProduct(Product product, int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("[ERROR] μΆ”κ°€ν•  μˆ˜λŸ‰μ€ 1개 이상이어야 ν•©λ‹ˆλ‹€."); + } + + // κΈ°μ‘΄ 동일 μƒν’ˆμ΄ μžˆλŠ”μ§€ 확인 ν›„ μˆ˜λŸ‰ 증가 + items.put(product, items.getOrDefault(product, 0) + quantity); + } + + public void addFreeItem(Product product, int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException("[ERROR] μΆ”κ°€ν•  증정 μˆ˜λŸ‰μ΄ 0개 μ΄ν•˜μΌ 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + System.out.println(product.getName() + " " + quantity + "κ°œκ°€ 무료둜 μ¦μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); + addProduct(product, quantity); + } + + public void removeItem(Product product, int quantity) { + if (!items.containsKey(product)) { + throw new IllegalArgumentException("[ERROR] ν•΄λ‹Ή μƒν’ˆμ€ μž₯λ°”κ΅¬λ‹ˆμ— μ—†μŠ΅λ‹ˆλ‹€."); + } + int currentQuantity = items.get(product); + if (quantity > currentQuantity) { + throw new IllegalArgumentException("[ERROR] μ œκ±°ν•  μˆ˜λŸ‰μ΄ μž₯λ°”κ΅¬λ‹ˆμ— μžˆλŠ” μˆ˜λŸ‰λ³΄λ‹€ λ§ŽμŠ΅λ‹ˆλ‹€."); + } + if (quantity == currentQuantity) { + items.remove(product); // μˆ˜λŸ‰μ΄ 0이면 μž₯λ°”κ΅¬λ‹ˆμ—μ„œ 제거 + } else { + items.put(product, currentQuantity - quantity); + } + System.out.println(product.getName() + " " + quantity + "κ°œκ°€ μž₯λ°”κ΅¬λ‹ˆμ—μ„œ μ œκ±°λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); + } + + public Map getItems() { + return items; + } +} \ No newline at end of file diff --git a/src/main/java/store/model/dto/FreeItem.java b/src/main/java/store/model/dto/FreeItem.java new file mode 100644 index 0000000..829bf02 --- /dev/null +++ b/src/main/java/store/model/dto/FreeItem.java @@ -0,0 +1,19 @@ +package store.model.dto; + +public class FreeItem { + private final String productName; + private final int quantity; + + public FreeItem(String productName, int quantity) { + this.productName = productName; + this.quantity = quantity; + } + + public String getProductName() { + return productName; + } + + public int getQuantity() { + return quantity; + } +} \ No newline at end of file diff --git a/src/main/java/store/model/dto/PurchaseItem.java b/src/main/java/store/model/dto/PurchaseItem.java new file mode 100644 index 0000000..f505fd8 --- /dev/null +++ b/src/main/java/store/model/dto/PurchaseItem.java @@ -0,0 +1,25 @@ +package store.model.dto; + +public class PurchaseItem { + private final String productName; + private final int quantity; + private final int totalPrice; + + public PurchaseItem(String productName, int quantity, int totalPrice) { + this.productName = productName; + this.quantity = quantity; + this.totalPrice = totalPrice; + } + + public String getProductName() { + return productName; + } + + public int getQuantity() { + return quantity; + } + + public int getTotalPrice() { + return totalPrice; + } +} \ No newline at end of file diff --git a/src/main/java/store/service/CheckoutService.java b/src/main/java/store/service/CheckoutService.java new file mode 100644 index 0000000..4784f13 --- /dev/null +++ b/src/main/java/store/service/CheckoutService.java @@ -0,0 +1,71 @@ +package store.service; + +import store.model.domain.Receipt; +import store.model.domain.ShoppingCart; +import store.model.domain.Product; +import store.model.dto.PurchaseItem; +import store.model.dto.FreeItem; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + + +public class CheckoutService { + private static final double MEMBERSHIP_DISCOUNT_RATE = 0.3; + private static final int MAX_MEMBERSHIP_DISCOUNT = 8000; + + public Receipt checkout(ShoppingCart cart, boolean isMembership) { + Map items = cart.getItems(); + + if (items.isEmpty()) { + throw new IllegalStateException("[ERROR] μž₯λ°”κ΅¬λ‹ˆκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€. ꡬ맀할 μƒν’ˆμ„ μΆ”κ°€ν•˜μ„Έμš”."); + } + + List purchasedItems = items.entrySet().stream() + .map(entry -> new PurchaseItem( + entry.getKey().getName(), + entry.getValue(), + entry.getKey().getPrice() * entry.getValue())) + .collect(Collectors.toList()); + + int totalAmount = purchasedItems.stream() + .mapToInt(PurchaseItem::getTotalPrice) + .sum(); + + List freeItems = items.entrySet().stream() + .map(entry -> { + Product product = entry.getKey(); + int quantity = entry.getValue(); + + boolean isPromotionApplied = PromotionCalculator.applyPromotion(product, quantity); + + if (isPromotionApplied) { + return new FreeItem(product.getName(), quantity / product.getPromotion().getBuyCount()); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + int promotionDiscount = freeItems.stream() + .mapToInt(item -> { + Product product = items.keySet().stream() + .filter(p -> p.getName().equals(item.getProductName())) + .findFirst() + .orElse(null); + return (product != null ? product.getPrice() : 0) * item.getQuantity(); + }) + .sum(); + + int discountBase = Math.max(0, totalAmount - promotionDiscount); + int membershipDiscount = isMembership + ? Math.min((int) (discountBase * MEMBERSHIP_DISCOUNT_RATE), MAX_MEMBERSHIP_DISCOUNT) + : 0; + + int finalAmount = totalAmount - promotionDiscount - membershipDiscount; + + return new Receipt(purchasedItems, freeItems, totalAmount, promotionDiscount, membershipDiscount , finalAmount); + } +} \ No newline at end of file diff --git a/src/main/java/store/service/ProductService.java b/src/main/java/store/service/ProductService.java new file mode 100644 index 0000000..85fdc02 --- /dev/null +++ b/src/main/java/store/service/ProductService.java @@ -0,0 +1,37 @@ +package store.service; + +import java.util.Optional; +import store.model.domain.Product; +import store.model.domain.Promotion; +import store.utils.FileParser; +import java.util.List; + +public class ProductService { + private final List products; + + public ProductService() { + this.products = FileParser.parseProducts("products.md", List.of()); // βœ… Promotion 리슀트 전달 + } + + public void displayProducts() { + products.forEach(System.out::println); + } + + public List getProducts() { + return products; + } + + public Optional findProductByName(String name) { + return products.stream() + .filter(product -> product.getName().equals(name)) + .findFirst(); + } + +// public int getPromotionStock(Product product) { +// Promotion promotion = product.getPromotion(); +// if (promotion == null) { +// return 0; // ν”„λ‘œλͺ¨μ…˜μ΄ μ—†μœΌλ©΄ ν”„λ‘œλͺ¨μ…˜ 재고 μ—†μŒ +// } +// return product.getStock(); // 기본적으둜 재고λ₯Ό ν”„λ‘œλͺ¨μ…˜ 재고둜 ν™œμš© +// } +} \ No newline at end of file diff --git a/src/main/java/store/service/PromotionCalculator.java b/src/main/java/store/service/PromotionCalculator.java new file mode 100644 index 0000000..7d141ea --- /dev/null +++ b/src/main/java/store/service/PromotionCalculator.java @@ -0,0 +1,51 @@ +package store.service; + +import camp.nextstep.edu.missionutils.DateTimes; +import store.model.domain.Promotion; +import store.model.domain.Product; +import java.time.LocalDate; +import java.util.Optional; + +public class PromotionCalculator { + + /** + * βœ… ν”„λ‘œλͺ¨μ…˜ 적용 κ°€λŠ₯ μ—¬λΆ€ 확인 ν›„ 증정 μˆ˜λŸ‰ 계산 + */ + public static Optional calculateFreeItems(Product product, int quantity) { + Promotion promotion = product.getPromotion(); + + // βœ… `DateTimes.now()`μ—μ„œ λ‚ μ§œ λΆ€λΆ„λ§Œ μΆ”μΆœν•˜μ—¬ LocalDate둜 λ³€ν™˜ + LocalDate today = DateTimes.now().toLocalDate(); + + // βœ… 였늘 λ‚ μ§œκ°€ ν”„λ‘œλͺ¨μ…˜ κΈ°κ°„ 내에 ν¬ν•¨λ˜μ§€ μ•ŠμœΌλ©΄ ν”„λ‘œλͺ¨μ…˜ 적용 λΆˆκ°€ + if (promotion == null || !promotion.isActive(today)) { + return Optional.empty(); + } + + int buyCount = promotion.getBuyCount(); + int getCount = promotion.getGetCount(); + int freeQuantity = (quantity / buyCount) * getCount; + + return freeQuantity > 0 ? Optional.of(freeQuantity) : Optional.empty(); + } + + /** + * βœ… ν”„λ‘œλͺ¨μ…˜μ„ μ μš©ν•  수 μžˆλ‹€λ©΄, ProductServiceμ—μ„œ ν”„λ‘œλͺ¨μ…˜ 재고 확인 ν›„ 적용 + */ +// public static boolean applyPromotion(Product product, int quantity, ProductService productService) { +// Optional freeItemCount = calculateFreeItems(product, quantity); +// +// if (freeItemCount.isEmpty()) { +// return false; // ν”„λ‘œλͺ¨μ…˜ 적용 λΆˆκ°€ +// } +// +// int freeQuantity = freeItemCount.get(); +// int promotionStock = productService.getPromotionStock(product); +// +// return freeQuantity <= promotionStock; // ν”„λ‘œλͺ¨μ…˜ 재고 λΆ€μ‘± +// } + + public static boolean applyPromotion(Product product, int quantity) { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/store/service/PromotionService.java b/src/main/java/store/service/PromotionService.java new file mode 100644 index 0000000..76c00a4 --- /dev/null +++ b/src/main/java/store/service/PromotionService.java @@ -0,0 +1,82 @@ +package store.service; + +import store.model.domain.Promotion; +import store.model.domain.Product; +import store.model.domain.ShoppingCart; +import store.utils.FileParser; +import store.view.InputView; +import store.view.OutputView; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +public class PromotionService { + private final List promotions; + private final InputView inputView; + private final OutputView outputView; + private final ProductService productService; + + public PromotionService(ProductService productService, InputView inputView, OutputView outputView) { + this.promotions = FileParser.parsePromotions("promotions.md"); + this.productService = productService; // βœ… λͺ…μ‹œμ μœΌλ‘œ ProductService μ£Όμž… + this.inputView = inputView; + this.outputView = outputView; + } + + public List getPromotions() { + return promotions; + } + + public void applyPromotions(ShoppingCart cart) { + LocalDate today = LocalDate.now(); + + for (Map.Entry entry : cart.getItems().entrySet()) { + Product product = entry.getKey(); + int quantity = entry.getValue(); + Promotion promotion = product.getPromotion(); + + if (promotion == null || promotion.isActive(today)) { + continue; + } + + int buyCount = promotion.getBuyCount(); + int getCount = promotion.getGetCount(); + + // ν”„λ‘œλͺ¨μ…˜ 적용 κ°€λŠ₯ν•œ μΆ”κ°€ 증정 개수 계산 + int freeQuantity = (quantity / buyCount) * getCount; + if (freeQuantity > 0) { + int availableStock = product.getStock(); + freeQuantity = Math.min(freeQuantity, availableStock); + + if (freeQuantity > 0) { + outputView.askPromotionConfirmation(product.getName(), freeQuantity); + boolean confirm = inputView.askYesOrNo(); + + if (confirm) { + cart.addFreeItem(product, freeQuantity); + product.reduceStock(freeQuantity); + } + } + } + + // μΆ”κ°€ ꡬ맀 μ•ˆλ‚΄ + int requiredForPromotion = buyCount - (quantity % buyCount); + if (requiredForPromotion > 0 && requiredForPromotion != buyCount) { + outputView.suggestAdditionalPurchase(product.getName(), requiredForPromotion); + } + + // ν”„λ‘œλͺ¨μ…˜ 재고 λΆ€μ‘± μ‹œ μ •κ°€ 결제 μ—¬λΆ€ 확인 + int promoApplicableQuantity = (quantity / buyCount) * buyCount; + int nonPromoQuantity = quantity - promoApplicableQuantity; + if (nonPromoQuantity > 0) { + outputView.askNonPromotionPurchase(product.getName(), nonPromoQuantity); + boolean confirm = inputView.askYesOrNo(); + + if (!confirm) { + cart.removeItem(product, nonPromoQuantity); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/store/service/ShoppingCartService.java b/src/main/java/store/service/ShoppingCartService.java new file mode 100644 index 0000000..2e5672b --- /dev/null +++ b/src/main/java/store/service/ShoppingCartService.java @@ -0,0 +1,40 @@ +package store.service; + +import store.model.domain.ShoppingCart; +import store.model.domain.Product; +import store.utils.ShoppingCartParser; +import store.exception.InvalidInputException; + +import java.util.Map; +import java.util.Optional; + +public class ShoppingCartService { + private final ProductService productService; + + public ShoppingCartService(ProductService productService) { + this.productService = productService; + } + + /** + * βœ… μž₯λ°”κ΅¬λ‹ˆμ— μƒν’ˆ μΆ”κ°€ + */ + public ShoppingCart createShoppingCart(String input) { + Map parsedItems = ShoppingCartParser.parse(input); + ShoppingCart cart = new ShoppingCart(); + + for (Map.Entry entry : parsedItems.entrySet()) { + String productName = entry.getKey(); + int quantity = entry.getValue(); + + // βœ… μƒν’ˆμ„ ProductServiceμ—μ„œ 쑰회 + Optional productOpt = productService.findProductByName(productName); + if (productOpt.isEmpty()) { + throw new InvalidInputException("[ERROR] μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μƒν’ˆμž…λ‹ˆλ‹€: " + productName); + } + + cart.addProduct(productOpt.get(), quantity); + } + + return cart; + } +} \ No newline at end of file diff --git a/src/main/java/store/utils/DateUtils.java b/src/main/java/store/utils/DateUtils.java new file mode 100644 index 0000000..d7184cf --- /dev/null +++ b/src/main/java/store/utils/DateUtils.java @@ -0,0 +1,4 @@ +package store.utils; + +public class DateUtils { +} diff --git a/src/main/java/store/utils/FileParser.java b/src/main/java/store/utils/FileParser.java new file mode 100644 index 0000000..b23cba1 --- /dev/null +++ b/src/main/java/store/utils/FileParser.java @@ -0,0 +1,89 @@ +package store.utils; + +import java.util.Arrays; +import store.model.domain.Product; +import store.model.domain.Promotion; +import store.enums.PromotionType; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +public class FileParser { + public static List parseProducts(String filePath, List promotions) { + try (BufferedReader reader = getBufferedReader(filePath)) { + return reader.lines() + .skip(1) // 첫 번째 쀄(헀더) κ±΄λ„ˆλ›°κΈ° + .map(line -> parseProduct(line, promotions)) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new IllegalStateException("[ERROR] νŒŒμΌμ„ 읽을 수 μ—†μŠ΅λ‹ˆλ‹€: " + filePath, e); + } + } + + public static List parsePromotions(String filePath) { + try (BufferedReader reader = getBufferedReader(filePath)) { + return reader.lines() + .skip(1) + .map(FileParser::parsePromotion) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new IllegalStateException("[ERROR] νŒŒμΌμ„ 읽을 수 μ—†μŠ΅λ‹ˆλ‹€: " + filePath, e); + } + } + + private static BufferedReader getBufferedReader(String filePath) { + ClassLoader classLoader = FileParser.class.getClassLoader(); + InputStream inputStream = classLoader.getResourceAsStream(filePath); + + if (inputStream == null) { + throw new IllegalStateException("[ERROR] λ¦¬μ†ŒμŠ€ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: " + filePath); + } + + return new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + } + + /** + * βœ… `promotionStock`을 κ³ λ €ν•œ Product 객체 생성 + */ + private static Product parseProduct(String line, List promotions) { + String[] parts = line.split(","); + + if (parts.length != 4) { + throw new IllegalArgumentException("[ERROR] μƒν’ˆ 데이터 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€: " + line); + } + + String name = parts[0].trim(); + int price = Integer.parseInt(parts[1].trim()); + int stock = Integer.parseInt(parts[2].trim()); + String promotionName = parts[3].trim(); + + System.out.println("[DEBUG] Extracting - Name: " + name + ", Price: " + price + ", Stock: " + stock+ ", Promotion: " + promotionName); + + Promotion promotion = promotions.stream() + .filter(p -> p.getName().equals(promotionName)) + .findFirst() + .orElse(null); + + return new Product(name, price, stock, promotion); // βœ… promotionStock 포함 + } + + private static Promotion parsePromotion(String line) { + String[] parts = line.split(","); + if (parts.length < 5) { + throw new IllegalArgumentException("[ERROR] ν”„λ‘œλͺ¨μ…˜ 데이터 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + + String name = parts[0].trim(); + int buyCount = Integer.parseInt(parts[1].trim()); + int getCount = Integer.parseInt(parts[2].trim()); + String startDate = parts[3].trim(); + String endDate = parts[4].trim(); + + return new Promotion(name, buyCount, getCount, startDate, endDate); + } +} \ No newline at end of file diff --git a/src/main/java/store/utils/ShoppingCartParser.java b/src/main/java/store/utils/ShoppingCartParser.java new file mode 100644 index 0000000..291617c --- /dev/null +++ b/src/main/java/store/utils/ShoppingCartParser.java @@ -0,0 +1,44 @@ +package store.utils; + +import store.exception.InvalidInputException; +import java.util.HashMap; +import java.util.Map; + +public class ShoppingCartParser { + + /** + * βœ… μž…λ ₯값을 κ²€μ¦ν•˜κ³  μƒν’ˆλͺ…-μˆ˜λŸ‰μ„ Map으둜 λ°˜ν™˜ + */ + public static Map parse(String input) { + // βœ… μž…λ ₯값이 λΉ„μ–΄μžˆλŠ” 경우 μ˜ˆμ™Έ λ°œμƒ + if (input.trim().isEmpty()) { + throw new InvalidInputException("[ERROR] μž…λ ₯값이 λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€. μ˜¬λ°”λ₯Έ ν˜•μ‹μœΌλ‘œ μž…λ ₯ν•˜μ„Έμš”."); + } + + Map parsedItems = new HashMap<>(); + String[] items = input.replaceAll("[\\[\\]]", "").split(","); + + for (String item : items) { + String[] parts = item.split("-"); + if (parts.length != 2) { + throw new InvalidInputException("[ERROR] μ˜¬λ°”λ₯Έ μž…λ ₯ ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€. 예: [콜라-3],[사이닀-1]"); + } + + String productName = parts[0].trim(); + int quantity; + try { + quantity = Integer.parseInt(parts[1].trim()); + if (quantity <= 0) { + throw new InvalidInputException("[ERROR] μˆ˜λŸ‰μ€ 1 이상이어야 ν•©λ‹ˆλ‹€."); + } + } catch (NumberFormatException e) { + throw new InvalidInputException("[ERROR] μˆ˜λŸ‰μ€ 숫자둜 μž…λ ₯ν•΄μ•Ό ν•©λ‹ˆλ‹€."); + } + + // βœ… Map에 μƒν’ˆλͺ…κ³Ό μˆ˜λŸ‰ μ €μž₯ + parsedItems.put(productName, parsedItems.getOrDefault(productName, 0) + quantity); + } + + return parsedItems; + } +} \ No newline at end of file diff --git a/src/main/java/store/validator/InputValidator.java b/src/main/java/store/validator/InputValidator.java new file mode 100644 index 0000000..59e55aa --- /dev/null +++ b/src/main/java/store/validator/InputValidator.java @@ -0,0 +1,11 @@ +package store.validator; + +import store.exception.InvalidInputException; + +public class InputValidator { + public static void validateProductSelection(String input) { + if (!input.matches("\\[.*-\\d+]")) { + throw new InvalidInputException("[ERROR] μ˜¬λ°”λ₯Έ μž…λ ₯ ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€."); + } + } +} \ No newline at end of file diff --git a/src/main/java/store/view/InputView.java b/src/main/java/store/view/InputView.java new file mode 100644 index 0000000..d9098b4 --- /dev/null +++ b/src/main/java/store/view/InputView.java @@ -0,0 +1,47 @@ +package store.view; + +import camp.nextstep.edu.missionutils.Console; +import store.model.domain.ShoppingCart; +import store.service.ShoppingCartService; +import store.exception.InvalidInputException; + +public class InputView { + private final ShoppingCartService shoppingCartService; + + public InputView(ShoppingCartService shoppingCartService) { + this.shoppingCartService = shoppingCartService; + } + + public ShoppingCart getUserShoppingCart() { + System.out.println("\nꡬ맀할 μƒν’ˆλͺ…κ³Ό μˆ˜λŸ‰μ„ μž…λ ₯ν•΄ μ£Όμ„Έμš”. (예: [사이닀-2],[감자칩-1])"); + + while (true) { + try { + String input = Console.readLine(); + return shoppingCartService.createShoppingCart(input); // βœ… ShoppingCartService ν™œμš© + } catch (InvalidInputException e) { + System.out.println(e.getMessage()); + } + } + } + + public boolean askMembership() { + System.out.println("\n멀버십 할인을 λ°›μœΌμ‹œκ² μŠ΅λ‹ˆκΉŒ? (Y/N)"); + return askYesOrNo(); + } + + public boolean askYesOrNo() { + String input; + while (true) { + input = Console.readLine().trim().toUpperCase(); + if (input.equals("Y")) return true; + if (input.equals("N")) return false; + System.out.println("[ERROR] Y λ˜λŠ” N만 μž…λ ₯ν•΄μ£Όμ„Έμš”."); + } + } + + public boolean askContinueShopping() { + System.out.println("κ°μ‚¬ν•©λ‹ˆλ‹€. κ΅¬λ§€ν•˜κ³  싢은 λ‹€λ₯Έ μƒν’ˆμ΄ μžˆλ‚˜μš”? (Y/N)"); + return askYesOrNo(); + } +} \ No newline at end of file diff --git a/src/main/java/store/view/OutputView.java b/src/main/java/store/view/OutputView.java new file mode 100644 index 0000000..4b13398 --- /dev/null +++ b/src/main/java/store/view/OutputView.java @@ -0,0 +1,46 @@ +package store.view; + +import store.model.domain.Receipt; +import store.model.dto.FreeItem; +import store.model.dto.PurchaseItem; + +public class OutputView { + public void displayWelcomeMessage() { + System.out.println("μ•ˆλ…•ν•˜μ„Έμš”. W νŽΈμ˜μ μž…λ‹ˆλ‹€."); + System.out.println("ν˜„μž¬ λ³΄μœ ν•˜κ³  μžˆλŠ” μƒν’ˆμž…λ‹ˆλ‹€.\n"); + } + + public void displayReceipt(Receipt receipt) { + System.out.println("\n=========== W 편의점 ============="); + + System.out.println("μƒν’ˆλͺ… μˆ˜λŸ‰ κΈˆμ•‘"); + for (PurchaseItem item : receipt.getPurchasedItems()) { + System.out.printf("%s %d %d%n", item.getProductName(), item.getQuantity(), item.getTotalPrice()); + } + + if (!receipt.getFreeItems().isEmpty()) { + System.out.println("=========== 증정 ============="); + for (FreeItem item : receipt.getFreeItems()) { + System.out.printf("%s %d%n", item.getProductName(), item.getQuantity()); + } + } + + System.out.println("=============================="); + System.out.printf("총ꡬ맀앑: %,d%n", receipt.getTotalAmount()); + System.out.printf("행사할인: -%,d%n", receipt.getPromotionDiscount()); + System.out.printf("멀버십할인: -%,d%n", receipt.getMembershipDiscount()); + System.out.printf("λ‚΄μ‹€λˆ: %,d%n", receipt.getFinalAmount()); + System.out.println(); + } + public void askPromotionConfirmation(String productName, int freeQuantity) { + System.out.println("ν˜„μž¬ " + productName + "은(λŠ”) " + freeQuantity + "개λ₯Ό 무료둜 더 받을 수 μžˆμŠ΅λ‹ˆλ‹€. μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? (Y/N)"); + } + + public void suggestAdditionalPurchase(String productName, int requiredForPromotion) { + System.out.println("ν˜„μž¬ " + productName + "을(λ₯Ό) " + requiredForPromotion + "개 더 κ΅¬λ§€ν•˜λ©΄ ν˜œνƒμ„ 받을 수 μžˆμŠ΅λ‹ˆλ‹€."); + } + + public void askNonPromotionPurchase(String productName, int nonPromoQuantity) { + System.out.println("ν˜„μž¬ " + productName + " " + nonPromoQuantity + "κ°œλŠ” ν”„λ‘œλͺ¨μ…˜ 할인이 μ μš©λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. κ·Έλž˜λ„ κ΅¬λ§€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ? (Y/N)"); + } +} \ No newline at end of file diff --git a/src/main/resources/promotions.md b/src/main/resources/promotions.md index d89887c..128b576 100644 --- a/src/main/resources/promotions.md +++ b/src/main/resources/promotions.md @@ -1,4 +1,4 @@ name,buy,get,start_date,end_date -탄산2+1,2,1,2024-01-01,2024-12-31 -MDμΆ”μ²œμƒν’ˆ,1,1,2024-01-01,2024-12-31 -λ°˜μ§ν• μΈ,1,1,2024-11-01,2024-11-30 +탄산2+1,2,1,2024-01-01,2025-03-08 +MDμΆ”μ²œμƒν’ˆ,1,1,2024-01-01,2025-03-08 +λ°˜μ§ν• μΈ,1,1,2024-11-01,2025-04-10