diff --git a/README.md b/README.md index 8cb9b8eb..70bb3a28 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,46 @@ -![문제 원문1](description-img/4.편의점_page-0001.jpg) -![문제 원문2](description-img/4.편의점_page-0002.jpg) -![문제 원문3](description-img/4.편의점_page-0003.jpg) -![문제 원문4](description-img/4.편의점_page-0004.jpg) -![문제 원문5](description-img/4.편의점_page-0005.jpg) -![문제 원문6](description-img/4.편의점_page-0006.jpg) -![문제 원문7](description-img/4.편의점_page-0007.jpg) -![문제 원문8](description-img/4.편의점_page-0008.jpg) -![문제 원문9](description-img/4.편의점_page-0009.jpg) -![문제 원문10](description-img/4.편의점_page-0010.jpg) +# 기능 구현 목록 +1. 환영 인사와 재고 상태를 출력한다. + 1. 프로모션 파일을 읽어서 저장한다. + 2. 재고 파일을 읽어서 저장 후 출력한다. +2. 구매 상품명과 수량을 입력받는다. + - 예외 사항 + 1. 빈 문자열 입력 시 예외 발생 + 2. 형식에 맞지 않으면 예외 발생 +3. 구매 상품명과 수량을 각각 문자열과 숫자로 변환한다. + - 예외 사항 + 1. 수량이 1 이상이 아니면 예외 발생 + 2. 존재하지 않는 상품을 입력하면 예외 발생 + 3. 구매 수량이 재고 수량을 초과하면 예외 발생 +4. 프로모션 적용이 가능한 상품에 대해 고객이 해당 수량보다 적게 가져온 경우, 필요한 수량을 추가로 가져오면 혜택을 받을 수 있음을 안내한다.(해당 상품 종류의 개수만큼 안내 내용을 출력한다.) + - Y + 1. 무료로 더 받을 수 있는 개수만큼 구매 및 증정 상품 목록에 추가한다. + - N + - 다음 안내로 넘어간다. +5. 프로모션 재고가 부족하여 일부 수령을 프로모션 혜택 없이 결제해야 하는 경우, 일부 수량에 대해 정가로 결제하게 됨을 안내한다.(해당 상품 종류의 개수만큼 안내 내용을 출력한다.) + - Y + 1. 프로모션 혜택 없이 일부 수량에 대해 정가로 계산한다. + 2. 구매 및 증정 상품 목록에 추가한다. + - N + 1. 프로모션 혜택 없는 일부 수량을 제외한 나머지를 구매 및 증정 상품 목록에 추가한다. +6. 5, 6번에 해당하지 않는 프로모션 상품을 구매 및 증정 상품 목록에 추가한다. +7. 프로모션에 해당하지 않는 상품을 구매 상품 목록에 추가한다. + - 프로모션 해당하지 않는 상품: 프로모션 값이 null이거나 프로모션 기간에 오늘 날짜가 포함되지 않는 경우 +8. 멤버십 할인을 받을지 유무를 입력받는다. + - Y + 1. 프로모션 적용되지 않은 상품에 대한 총 금액의 30%를 멤버십 할인 금액으로 저장한다. + 2. 만약 멤버십 할인 금액이 8,000원 초과면 멤버십 할인 금액을 8,000원으로 저장한다. + - N + 1. 다음으로 넘어간다. +9. 구매 및 증정 목록에 기반하여 재고를 업데이트한다. +10. 영수증을 출력한다. + - 전체 구매 상품명 및 수량 + - 증정 상품명 및 수량 + - 총 구매액 + - 행사 할인 금액 + - 멤버십 할인 금액 + - 내실돈 +11. 다른 상품을 구매할지 여부를 입력받는다. + - Y + 1. 1번부터 다시 시작 + - N + 1. 프로그램 종료 \ No newline at end of file diff --git a/src/main/java/store/Application.java b/src/main/java/store/Application.java index ec4afd8b..ce5b6b18 100644 --- a/src/main/java/store/Application.java +++ b/src/main/java/store/Application.java @@ -1,7 +1,14 @@ package store; +import java.io.IOException; +import store.controller.StoreController; +import store.service.StoreService; + public class Application { - public static void main(String[] args) { - // TODO: 프로그램 구현 + + public static void main(String[] args) throws IOException { + StoreService storeService = new StoreService(); + StoreController storeController = new StoreController(storeService); + storeController.run(); } } diff --git a/src/main/java/store/constant/Constant.java b/src/main/java/store/constant/Constant.java new file mode 100644 index 00000000..2920f810 --- /dev/null +++ b/src/main/java/store/constant/Constant.java @@ -0,0 +1,7 @@ +package store.constant; + +public final class Constant { + +// public static final int = ; +// public static final String = ; +} diff --git a/src/main/java/store/constant/ErrorMessage.java b/src/main/java/store/constant/ErrorMessage.java new file mode 100644 index 00000000..424e0689 --- /dev/null +++ b/src/main/java/store/constant/ErrorMessage.java @@ -0,0 +1,20 @@ +package store.constant; + +public enum ErrorMessage { + + FORMAT_ERROR("올바르지 않은 형식으로 입력했습니다. 다시 입력해 주세요."), + NO_EXIST_ERROR("존재하지 않는 상품입니다. 다시 입력해 주세요."), + EXCEED_ERROR("재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."), + INVALID_ERROR("잘못된 입력입니다. 다시 입력해 주세요."); + + private static final String ERROR_MESSAGE_PREFIX = "[ERROR] "; + private final String errorMessage; + + ErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorMessage() { + return ERROR_MESSAGE_PREFIX + errorMessage; + } +} diff --git a/src/main/java/store/controller/StoreController.java b/src/main/java/store/controller/StoreController.java new file mode 100644 index 00000000..b6897770 --- /dev/null +++ b/src/main/java/store/controller/StoreController.java @@ -0,0 +1,140 @@ +package store.controller; + +import java.io.IOException; +import java.util.List; +import store.dto.ReceiptDto; +import store.dto.StockDto; +import store.service.StoreService; +import store.util.InputParser; +import store.util.file.FileReader; +import store.util.file.FileWriter; +import store.view.InputView; +import store.view.OutputView; + +public class StoreController { + + private final StoreService storeService; + + public StoreController(StoreService storeService) { + this.storeService = storeService; + } + + public void run() throws IOException { + while (true) { + readFiles(); + + StockDto stockDto = storeService.getStockDto(); + OutputView.printStock(stockDto); + + registerPurchaseProducts(); + + handleFreeProducts(); + handleOverProducts(); + handleNormalProducts(); + handleNoPromotionProducts(); + setMembershipDiscountAmount(); + + writeStockFile(); + printResult(); + + String rawChoice = InputView.readMorePurchaseChoice(); + boolean choice = InputParser.parseChoice(rawChoice); + if (!choice) { + break; + } + } + } + + private void readFiles() throws IOException { + FileReader promotionReader = new FileReader("src/main/resources/promotions.md"); + List readPromotions = promotionReader.readLines(); + storeService.readPromotionFile(readPromotions); + + FileReader fr = new FileReader("src/main/resources/products.md"); + List readProducts = fr.readLines(); + storeService.readProductsFile(readProducts); + } + + private void registerPurchaseProducts() { + while (true) { + try { + storeService.registerPurchaseProducts(); + return; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void handleFreeProducts() { + while (true) { + try { + storeService.handleFreeProducts(); + return; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void handleOverProducts() { + while (true) { + try { + storeService.handleOverProducts(); + return; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void handleNormalProducts() { + while (true) { + try { + storeService.handleNormalProducts(); + return; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void handleNoPromotionProducts() { + while (true) { + try { + storeService.handleNoPromotionProducts(); + return; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void setMembershipDiscountAmount() { + while (true) { + try { + String rawChoice = InputView.readMembershipChoice(); + boolean choice = InputParser.parseChoice(rawChoice); + + if (choice) { + storeService.setMembershipDiscountAmount(); + } + break; + } catch (IllegalArgumentException e) { + OutputView.printErrorMessage(e); + } + } + } + + private void writeStockFile() throws IOException { + FileWriter fw = new FileWriter("src/main/resources/products.md"); + + String stockResult = storeService.getStockResult(); + fw.writeAll(stockResult); + } + + private void printResult() { + ReceiptDto receiptDto = storeService.getReceiptResult(); + OutputView.printReceipt(receiptDto); + } +} diff --git a/src/main/java/store/domain/Customer.java b/src/main/java/store/domain/Customer.java new file mode 100644 index 00000000..402d843d --- /dev/null +++ b/src/main/java/store/domain/Customer.java @@ -0,0 +1,79 @@ +package store.domain; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import store.dto.ReceiptDto; + +public class Customer { + + private final Map> purchaseProducts; + private int membershipDiscountAmount; + + private Customer() { + purchaseProducts = new LinkedHashMap<>(); + } + + public static Customer newInstance() { + return new Customer(); + } + + public void addProductForPromotion(Product product, int purchaseQuantity) { + int buy = product.getPromotion().getBuy(); + int get = product.getPromotion().getGet(); + int setSize = buy + get; + int setQuantity = purchaseQuantity / setSize; + int setQuantityNotEqual = product.getPromotionQuantity() / setSize; + if (setQuantity > setQuantityNotEqual) { + setQuantity = setQuantityNotEqual; + } + int presentQuantity = get * setQuantity; + int noPromotionQuantity = purchaseQuantity - setSize * setQuantity; + + purchaseProducts.put(product, List.of(purchaseQuantity, presentQuantity, noPromotionQuantity)); + product.updateStock(purchaseQuantity); + } + + public void addProductForNormal(Product product, int purchaseQuantity) { + purchaseProducts.put(product, List.of(purchaseQuantity, 0, purchaseQuantity)); + product.updateStock(purchaseQuantity); + } + + public void setMembershipDiscountAmount() { + membershipDiscountAmount = calculateMembershipDiscountAmount(); + if (membershipDiscountAmount > 8000) { + membershipDiscountAmount = 8000; + } + } + + private int calculateMembershipDiscountAmount() { + int sum = 0; + for (Product product : purchaseProducts.keySet()) { + int price = product.getPrice(); + int noPromotionQuantity = purchaseProducts.get(product).get(2); + sum += price * noPromotionQuantity; + } + return (int) (sum * 0.3); + } + + public Map> getPurchaseProducts() { + return purchaseProducts; + } + + public ReceiptDto getReceipt() { + Map purchaseProducts = new LinkedHashMap<>(); + Map presentProducts = new LinkedHashMap<>(); + for (Product product : this.purchaseProducts.keySet()) { + List values = this.purchaseProducts.get(product); + + int purchaseQuantity = values.get(0); + purchaseProducts.put(product, purchaseQuantity); + + int presentQuantity = values.get(1); + if (presentQuantity > 0) { + presentProducts.put(product, presentQuantity); + } + } + return new ReceiptDto(purchaseProducts, presentProducts, membershipDiscountAmount); + } +} diff --git a/src/main/java/store/domain/Product.java b/src/main/java/store/domain/Product.java new file mode 100644 index 00000000..2c660d69 --- /dev/null +++ b/src/main/java/store/domain/Product.java @@ -0,0 +1,166 @@ +package store.domain; + +import java.time.LocalDate; +import java.util.Objects; +import store.time.DateTime; + +public class Product { + + private final String name; + private final int price; + private int promotionQuantity; + private int normalQuantity; + private final Promotion promotion; + private boolean isChecked; + + private Product(String name, int price, int promotionQuantity, int normalQuantity, Promotion promotion) { + this.name = name; + this.price = price; + this.promotionQuantity = promotionQuantity; + this.normalQuantity = normalQuantity; + this.promotion = promotion; + } + + public static Product of(String name, int price, int quantity, Promotion promotion) { + if (promotion == null) { + return new Product(name, price, 0, quantity, promotion); + } + return new Product(name, price, quantity, 0, promotion); + } + + public void setQuantity(int quantity, Promotion promotion) { + if (promotion == null) { + normalQuantity = quantity; + return; + } + promotionQuantity = quantity; + } + + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) { + return false; + } + Product product = (Product) object; + return Objects.equals(name, product.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public int getPrice(int quantity) { + return price * quantity; + } + + public int getPromotionQuantity() { + return promotionQuantity; + } + + public int getNormalQuantity() { + return normalQuantity; + } + + public Promotion getPromotion() { + return promotion; + } + + public String getNormalQuantityForOutput() { + if (normalQuantity == 0) { + return "재고 없음"; + } + return normalQuantity + "개"; + } + + public String getPriceForOutput() { + return String.format("%,d원", price); + } + + public String getPromotionForOutput() { + if (promotion == null) { + return ""; + } + return promotion.getName(); + } + + public String getPromotionQuantityForOutput() { + if (promotionQuantity == 0) { + return "재고 없음"; + } + return promotionQuantity + "개"; + } + + public boolean isPromotion() { + LocalDate now = DateTime.now(); + return promotion != null + && !now.isBefore(promotion.getStartDate()) + && !now.isAfter(promotion.getEndDate()); + } + + public boolean isLessPromotionProduct(int purchaseQuantity) { + int buy = promotion.getBuy(); + int get = promotion.getGet(); + int mod = purchaseQuantity % (buy + get); + + if (purchaseQuantity >= promotionQuantity) { + return false; + } + + int maxPurchaseQuantity = purchaseQuantity + (buy + get - mod); + + return buy <= mod && maxPurchaseQuantity <= promotionQuantity; + } + + public boolean isMorePromotionProduct(int purchaseQuantity) { + int buy = promotion.getBuy(); + int get = promotion.getGet(); + int setSize = buy + get; + int mod = purchaseQuantity % setSize; + int minPromotionQuantity = purchaseQuantity - mod; + + return minPromotionQuantity > promotionQuantity; + } + + public int getFreeProductQuantity() { + return promotion.getGet(); + } + + public void updateStock(int quantity) { + if (this.promotionQuantity >= quantity) { + this.promotionQuantity -= quantity; + return; + } + + quantity -= this.promotionQuantity; + this.promotionQuantity = 0; + + this.normalQuantity -= quantity; + } + + public int getImpossiblePromotionQuantity(int purchaseQuantity) { + int buy = promotion.getBuy(); + int get = promotion.getGet(); + int setSize = buy + get; + int mod = promotionQuantity % setSize; + int maxPromotionQuantity = promotionQuantity - mod; + + return purchaseQuantity - maxPromotionQuantity; + } + + public boolean isChecked() { + return isChecked; + } + + public void setCheckedTrue() { + isChecked = true; + } +} diff --git a/src/main/java/store/domain/Products.java b/src/main/java/store/domain/Products.java new file mode 100644 index 00000000..e645be01 --- /dev/null +++ b/src/main/java/store/domain/Products.java @@ -0,0 +1,83 @@ +package store.domain; + +import java.util.ArrayList; +import java.util.List; +import store.dto.StockDto; + +public class Products { + + private final List products; + + public Products() { + this.products = new ArrayList<>(); + } + + public static Products newInstance() { + return new Products(); + } + + public String getStockForFile() { + StringBuilder st = new StringBuilder(); + st.append("name,price,quantity,promotion\n"); + for (Product product : products) { + if (product.getPromotion() == null) { + st.append(product.getName() + "," + product.getPrice() + "," + product.getNormalQuantity() + "," + + "null" + "\n"); + continue; + } + st.append(product.getName() + "," + product.getPrice() + "," + product.getPromotionQuantity() + "," + + product.getPromotion().getName() + "\n"); + st.append(product.getName() + "," + product.getPrice() + "," + product.getNormalQuantity() + "," + "null" + + "\n"); + } + return st.toString(); + } + + public void addProduct(String name, int price, int quantity, Promotion promotion) { + for (Product product : products) { + if (product.getName().equals(name)) { + product.setQuantity(quantity, promotion); + return; + } + } + products.add(Product.of(name, price, quantity, promotion)); + } + + public StockDto getStockDto() { + StringBuilder st = new StringBuilder(); + for (Product product : products) { + if (product.getPromotion() == null) { + st.append("- " + product.getName() + " " + product.getPriceForOutput() + " " + product.getNormalQuantityForOutput() + " " + + "\n"); + continue; + } + st.append("- " + product.getName() + " " + product.getPriceForOutput() + " " + product.getPromotionQuantityForOutput() + " " + + product.getPromotionForOutput() + "\n"); + st.append("- " + product.getName() + " " + product.getPriceForOutput() + " " + product.getNormalQuantityForOutput() + " " + + "\n"); + } + return new StockDto(st.toString()); + } + + public boolean contains(String productName) { + for (Product product : products) { + if (product.getName().equals(productName)) { + return true; + } + } + return false; + } + + public int getQuantityForProduct(String productName) { + for (Product product : products) { + if (product.getName().equals(productName)) { + return product.getPromotionQuantity() + product.getNormalQuantity(); + } + } + return 0; + } + + public List getProducts() { + return products; + } +} diff --git a/src/main/java/store/domain/Promotion.java b/src/main/java/store/domain/Promotion.java new file mode 100644 index 00000000..bcffb76e --- /dev/null +++ b/src/main/java/store/domain/Promotion.java @@ -0,0 +1,63 @@ +package store.domain; + +import java.time.LocalDate; +import java.util.Objects; + +public class Promotion { + + private final String name; + private final int buy; + private final int get; + private final LocalDate startDate; + private final LocalDate endDate; + + private Promotion(String name, int buy, int get, LocalDate startDate, LocalDate endDate) { + this.name = name; + this.buy = buy; + this.get = get; + this.startDate = startDate; + this.endDate = endDate; + } + + public static Promotion of(String name, int buy, int get, LocalDate startDate, LocalDate endDate) { + return new Promotion(name, buy, get, startDate, endDate); + } + + public boolean equalsName(String name) { + return this.name.equals(name); + } + + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) { + return false; + } + Promotion promotion = (Promotion) object; + return Objects.equals(name, promotion.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + public int getBuy() { + return buy; + } + + public LocalDate getEndDate() { + return endDate; + } + + public int getGet() { + return get; + } + + public String getName() { + return name; + } + + public LocalDate getStartDate() { + return startDate; + } +} diff --git a/src/main/java/store/domain/Promotions.java b/src/main/java/store/domain/Promotions.java new file mode 100644 index 00000000..480c81d3 --- /dev/null +++ b/src/main/java/store/domain/Promotions.java @@ -0,0 +1,31 @@ +package store.domain; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public class Promotions { + + private final List promotions; + + private Promotions() { + this.promotions = new ArrayList<>(); + } + + public static Promotions newInstance() { + return new Promotions(); + } + + public void addPromotion(String name, int buy, int get, LocalDate startDate, LocalDate endDate) { + promotions.add(Promotion.of(name, buy, get, startDate, endDate)); + } + + public Promotion get(String promotionName) { + for (Promotion promotion : promotions) { + if (promotion.equalsName(promotionName)) { + return promotion; + } + } + return null; + } +} diff --git a/src/main/java/store/domain/vo/PurchaseProducts.java b/src/main/java/store/domain/vo/PurchaseProducts.java new file mode 100644 index 00000000..08bdc7e1 --- /dev/null +++ b/src/main/java/store/domain/vo/PurchaseProducts.java @@ -0,0 +1,132 @@ +package store.domain.vo; + +import static store.constant.ErrorMessage.EXCEED_ERROR; +import static store.constant.ErrorMessage.NO_EXIST_ERROR; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import store.domain.Product; +import store.domain.Products; + +public class PurchaseProducts { + + private final Map purchaseProducts; + + private PurchaseProducts() { + this.purchaseProducts = new LinkedHashMap<>(); + } + + public static PurchaseProducts newInstance() { + return new PurchaseProducts(); + } + + public void addProduct(Products products, String productName) { + validateNoExist(products, productName); + validateMax(products, productName); + purchaseProducts.put(productName, purchaseProducts.getOrDefault(productName, 0) + 1); + } + + private void validateMax(Products products, String productName) { + if (products.getQuantityForProduct(productName) <= purchaseProducts.getOrDefault(productName, 0)) { + throw new IllegalArgumentException(EXCEED_ERROR.getErrorMessage()); + } + } + + private void validateNoExist(Products products, String productName) { + if (!products.contains(productName)) { + throw new IllegalArgumentException(NO_EXIST_ERROR.getErrorMessage()); + } + } + + @Override + public String toString() { + return purchaseProducts.toString(); + } + + public List getLessPromotionProducts(Products products) { + List lessProducts = new ArrayList<>(); + for (Product product : products.getProducts()) { + // 이미 계산한 상품인지 확인 + if (product.isChecked()) { + continue; + } + + // 프로모션인지 또는 구매 상품인지 판단 + if (!product.isPromotion() || !purchaseProducts.containsKey(product.getName())) { + continue; + } + + // 주문 개수 < 프로모션 적용 개수인 상품은 주문 개수를 값으로 저장 + if (product.isLessPromotionProduct(purchaseProducts.get(product.getName()))) { + lessProducts.add(product); + product.setCheckedTrue(); + } + } + return lessProducts; + } + + public List getMorePromotionProducts(Products products) { + List moreProducts = new ArrayList<>(); + for (Product product : products.getProducts()) { + // 이미 계산한 상품인지 확인 + if (product.isChecked()) { + continue; + } + + // 프로모션인지 또는 구매 상품인지 판단 + if (!product.isPromotion() || !purchaseProducts.containsKey(product.getName())) { + continue; + } + + if (product.isMorePromotionProduct(purchaseProducts.get(product.getName()))) { + moreProducts.add(product); + product.setCheckedTrue(); + } + } + return moreProducts; + } + + public int getPurchaseQuantity(Product product) { + return purchaseProducts.get(product.getName()); + } + + public List getNormalPromotionProducts(Products products) { + List normalPromotionProducts = new ArrayList<>(); + for (Product product : products.getProducts()) { + // 이미 계산한 상품인지 확인 + if (product.isChecked()) { + continue; + } + + // 프로모션인지 또는 구매 상품인지 판단 + if (!product.isPromotion() || !purchaseProducts.containsKey(product.getName())) { + continue; + } + + if (!product.isLessPromotionProduct(purchaseProducts.get(product.getName())) + && !product.isMorePromotionProduct(purchaseProducts.get(product.getName()))) { + normalPromotionProducts.add(product); + product.setCheckedTrue(); + } + } + return normalPromotionProducts; + } + + public List getNoPromotionProducts(Products products) { + List noPromotionProducts = new ArrayList<>(); + for (Product product : products.getProducts()) { + // 이미 계산한 상품인지 확인 + if (product.isChecked()) { + continue; + } + + if (!product.isPromotion() && purchaseProducts.containsKey(product.getName())) { + noPromotionProducts.add(product); + product.setCheckedTrue(); + } + } + return noPromotionProducts; + } +} diff --git a/src/main/java/store/dto/ReceiptDto.java b/src/main/java/store/dto/ReceiptDto.java new file mode 100644 index 00000000..5f885ec5 --- /dev/null +++ b/src/main/java/store/dto/ReceiptDto.java @@ -0,0 +1,9 @@ +package store.dto; + +import java.util.Map; +import store.domain.Product; + +public record ReceiptDto(Map purchaseProducts, Map presentProducts, + int membershipDiscountAmount) { + +} diff --git a/src/main/java/store/dto/StockDto.java b/src/main/java/store/dto/StockDto.java new file mode 100644 index 00000000..3ae797ae --- /dev/null +++ b/src/main/java/store/dto/StockDto.java @@ -0,0 +1,4 @@ +package store.dto; + +public record StockDto(String stock) { +} diff --git a/src/main/java/store/service/StoreService.java b/src/main/java/store/service/StoreService.java new file mode 100644 index 00000000..42db7ee6 --- /dev/null +++ b/src/main/java/store/service/StoreService.java @@ -0,0 +1,128 @@ +package store.service; + +import java.time.LocalDate; +import java.util.List; +import store.domain.Customer; +import store.domain.Product; +import store.domain.Products; +import store.domain.Promotion; +import store.domain.Promotions; +import store.domain.vo.PurchaseProducts; +import store.dto.ReceiptDto; +import store.dto.StockDto; +import store.util.InputParser; +import store.view.InputView; + +public class StoreService { + + private PurchaseProducts purchaseProducts; + private Products products; + private Promotions promotions; + private Customer customer; + + public void readPromotionFile(List readPromotions) { + readPromotions.removeFirst(); + promotions = Promotions.newInstance(); + for (String readPromotion : readPromotions) { + String[] split = readPromotion.split(","); + String name = split[0]; + int buy = Integer.parseInt(split[1]); + int get = Integer.parseInt(split[2]); + LocalDate startDate = LocalDate.parse(split[3]); + LocalDate endDate = LocalDate.parse(split[4]); + promotions.addPromotion(name, buy, get, startDate, endDate); + } + } + + public void readProductsFile(List readProducts) { + readProducts.removeFirst(); + products = Products.newInstance(); + for (String readProduct : readProducts) { + String[] split = readProduct.split(","); + String name = split[0]; + int price = Integer.parseInt(split[1]); + int quantity = Integer.parseInt(split[2]); + Promotion promotion = promotions.get(split[3]); + products.addProduct(name, price, quantity, promotion); + } + } + + public StockDto getStockDto() { + return products.getStockDto(); + } + + public void registerPurchaseProducts() { + String rawPurchaseProducts = InputView.readPurchaseProducts(); + List parsePurchaseProducts = InputParser.parsePurchaseProducts(rawPurchaseProducts); + purchaseProducts = PurchaseProducts.newInstance(); + for (String productName : parsePurchaseProducts) { + purchaseProducts.addProduct(products, productName); + } + } + + public void handleFreeProducts() { + customer = Customer.newInstance(); + + List lessPromotionProducts = purchaseProducts.getLessPromotionProducts(products); + for (Product product : lessPromotionProducts) { + int purchaseQuantity = purchaseProducts.getPurchaseQuantity(product); + int free = product.getFreeProductQuantity(); + + String rawChoice = InputView.readFreeProductChoice(product.getName(), free); + boolean choice = InputParser.parseChoice(rawChoice); + + if (choice) { + customer.addProductForPromotion(product, purchaseQuantity + free); + continue; + } + customer.addProductForPromotion(product, purchaseQuantity); + } + } + + public void handleOverProducts() { + List morePromotionProducts = purchaseProducts.getMorePromotionProducts(products); + for (Product product : morePromotionProducts) { + int purchaseQuantity = purchaseProducts.getPurchaseQuantity(product); + int impossiblePromotionQuantity = product.getImpossiblePromotionQuantity(purchaseQuantity); + + String rawChoice = InputView.readImpossiblePromotionChoice(product.getName(), + impossiblePromotionQuantity); + boolean choice = InputParser.parseChoice(rawChoice); + + if (choice) { + customer.addProductForPromotion(product, purchaseQuantity); + continue; + } + customer.addProductForPromotion(product, purchaseQuantity - impossiblePromotionQuantity); + } + } + + + public void handleNormalProducts() { + List normalPromotionProducts = purchaseProducts.getNormalPromotionProducts(products); + for (Product product : normalPromotionProducts) { + int purchaseQuantity = purchaseProducts.getPurchaseQuantity(product); + customer.addProductForPromotion(product, purchaseQuantity); + } + } + + public void handleNoPromotionProducts() { + List noPromotionProducts = purchaseProducts.getNoPromotionProducts(products); + for (Product product : noPromotionProducts) { + int purchaseQuantity = purchaseProducts.getPurchaseQuantity(product); + customer.addProductForNormal(product, purchaseQuantity); + } + } + + public void setMembershipDiscountAmount() { + customer.setMembershipDiscountAmount(); + } + + public String getStockResult() { + return products.getStockForFile(); + } + + public ReceiptDto getReceiptResult() { + return customer.getReceipt(); + } +} diff --git a/src/main/java/store/time/DateTime.java b/src/main/java/store/time/DateTime.java new file mode 100644 index 00000000..2647ab6a --- /dev/null +++ b/src/main/java/store/time/DateTime.java @@ -0,0 +1,11 @@ +package store.time; + +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDate; + +public class DateTime { + + public static LocalDate now() { + return DateTimes.now().toLocalDate(); + } +} diff --git a/src/main/java/store/util/InputParser.java b/src/main/java/store/util/InputParser.java new file mode 100644 index 00000000..c107aab0 --- /dev/null +++ b/src/main/java/store/util/InputParser.java @@ -0,0 +1,45 @@ +package store.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public final class InputParser { + + private static final String FIRST_DELIMITER = ","; + private static final String SECOND_DELIMITER = "-"; + private static final String YES = "Y"; + + private InputParser() { + } + + public static List parsePurchaseProducts(String rawInput) { + Validator.validateNullOrBlank(rawInput); + rawInput = rawInput.strip(); + + Validator.validateCsvFormat(rawInput); + + List purchaseProducts = new ArrayList<>(); + String[] split = rawInput.split(FIRST_DELIMITER); + for (String s : split) { + String[] order = s.strip().substring(1, s.length() - 1).split(SECOND_DELIMITER); + String name = order[0]; + int quantity = NumberConvertor.convertToNumber(order[1]); + + Validator.validateQuantity(quantity); + + for (int i = 0; i < quantity; i++) { + purchaseProducts.add(name); + } + } + + return purchaseProducts; + } + + public static boolean parseChoice(String rawChoice) { + Validator.validateNullOrBlank(rawChoice); + rawChoice = rawChoice.strip(); + Validator.validateChoiceFormat(rawChoice); + return rawChoice.equals(YES); + } +} diff --git a/src/main/java/store/util/NumberConvertor.java b/src/main/java/store/util/NumberConvertor.java new file mode 100644 index 00000000..45682207 --- /dev/null +++ b/src/main/java/store/util/NumberConvertor.java @@ -0,0 +1,12 @@ +package store.util; + +public final class NumberConvertor { + + public static Integer convertToNumber(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/store/util/Validator.java b/src/main/java/store/util/Validator.java new file mode 100644 index 00000000..1ddb8bdd --- /dev/null +++ b/src/main/java/store/util/Validator.java @@ -0,0 +1,36 @@ +package store.util; + +import static store.constant.ErrorMessage.FORMAT_ERROR; +import static store.constant.ErrorMessage.INVALID_ERROR; + +public final class Validator { + + private static final String CSV_FORMAT = "^ *(\\[[가-힣a-zA-Z]+-\\d+])+ *(, *(\\[[가-힣a-zA-Z]+-\\d+])+ *)*$"; + private static final String YES_NO = "[YN]"; + + private Validator() {} + + public static void validateNullOrBlank(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(INVALID_ERROR.getErrorMessage()); + } + } + + public static void validateCsvFormat(String input) { + if (!input.matches(CSV_FORMAT)) { + throw new IllegalArgumentException(FORMAT_ERROR.getErrorMessage()); + } + } + + public static void validateQuantity(int quantity) { + if (quantity <= 0) { + throw new IllegalArgumentException(INVALID_ERROR.getErrorMessage()); + } + } + + public static void validateChoiceFormat(String rawChoice) { + if (!rawChoice.matches(YES_NO)) { + throw new IllegalArgumentException(INVALID_ERROR.getErrorMessage()); + } + } +} diff --git a/src/main/java/store/util/file/FileReader.java b/src/main/java/store/util/file/FileReader.java new file mode 100644 index 00000000..107cba3b --- /dev/null +++ b/src/main/java/store/util/file/FileReader.java @@ -0,0 +1,43 @@ +package store.util.file; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FileReader { + + private final java.io.FileReader fr; + + public FileReader(String fileName) throws IOException { + fr = new java.io.FileReader(fileName, UTF_8); + } + + // 파일 전체를 한 번에 읽기 + public String readAll() throws IOException { + StringBuilder contents = new StringBuilder(); + int ch; + while ((ch = fr.read()) != -1) { + contents.append((char) ch); + } + fr.close(); + + return contents.toString(); + } + + // 파일 전체를 한 줄 씩 나눠서 읽기 + public List readLines() throws IOException { + List contents = new ArrayList<>(); + BufferedReader br = new BufferedReader(fr); + + String line; + while ((line = br.readLine()) != null) { + contents.add(line); + } + br.close(); + + return new ArrayList<>(contents); + } +} diff --git a/src/main/java/store/util/file/FileWriter.java b/src/main/java/store/util/file/FileWriter.java new file mode 100644 index 00000000..c69bc15d --- /dev/null +++ b/src/main/java/store/util/file/FileWriter.java @@ -0,0 +1,43 @@ +package store.util.file; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; + +public class FileWriter { + + private final String fileName; + private java.io.FileWriter fw; + + public FileWriter(String fileName) { + this.fileName = fileName; + } + + // 파일에 내용 한 번에 쓰기 - append 기본값(false) + public void writeAll(String contents) throws IOException { + fw = new java.io.FileWriter(fileName, UTF_8, false); + fw.write(contents); + fw.close(); + } + + // 파일에 내용 한 번에 쓰기 - append 선택 + public void writeAll(String contents, boolean append) throws IOException { + fw = new java.io.FileWriter(fileName, UTF_8, append); + fw.write(contents); + fw.close(); + } + + // 파일에 내용 한 줄씩 쓰기 - append 기본값(false) + public void writeLine(String contents) throws IOException { + fw = new java.io.FileWriter(fileName, UTF_8, false); + fw.write(contents); + fw.close(); + } + + // 파일에 내용 한 줄씩 쓰기 - append 선택 + public void writeLine(String contents, boolean append) throws IOException { + fw = new java.io.FileWriter(fileName, UTF_8, append); + fw.write(contents); + fw.close(); + } +} diff --git a/src/main/java/store/view/InputView.java b/src/main/java/store/view/InputView.java new file mode 100644 index 00000000..2a4387ce --- /dev/null +++ b/src/main/java/store/view/InputView.java @@ -0,0 +1,37 @@ +package store.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + private static final String PURCHASE_PRODUCTS_REQUEST = "구매하실 상품명과 수량을 입력해주세요. (예: [사이다-2],[감자칩-1])"; + private static final String FREE_PRODUCTS_REQUEST = "\n현재 %s은(는) %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (Y/N)\n"; + private static final String IMPOSSIBLE_PROMOTION_REQUEST = "\n현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (Y/N)\n"; + private static final String MEMBERSHIP_REQUEST = "\n멤버십 할인을 받으시겠습니까? (Y/N)"; + private static final String MORE_PURCHASE_REQUEST = "\n감사합니다. 구매하고 싶은 다른 상품이 있나요? (Y/N)"; + + public static String readPurchaseProducts() { + System.out.println(PURCHASE_PRODUCTS_REQUEST); + return Console.readLine(); + } + + public static String readFreeProductChoice(String name, int free) { + System.out.printf(FREE_PRODUCTS_REQUEST, name, free); + return Console.readLine(); + } + + public static String readImpossiblePromotionChoice(String name, int impossiblePromotionQuantity) { + System.out.printf(IMPOSSIBLE_PROMOTION_REQUEST, name, impossiblePromotionQuantity); + return Console.readLine(); + } + + public static String readMembershipChoice() { + System.out.println(MEMBERSHIP_REQUEST); + return Console.readLine(); + } + + public static String readMorePurchaseChoice() { + System.out.println(MORE_PURCHASE_REQUEST); + return Console.readLine(); + } +} diff --git a/src/main/java/store/view/OutputView.java b/src/main/java/store/view/OutputView.java new file mode 100644 index 00000000..0e0b2fd2 --- /dev/null +++ b/src/main/java/store/view/OutputView.java @@ -0,0 +1,58 @@ +package store.view; + +import java.util.Map; +import store.domain.Product; +import store.dto.ReceiptDto; +import store.dto.StockDto; + +public class OutputView { + + private static final String WELL_COME_MESSAGE = "\n안녕하세요. W편의점입니다.\n현재 보유하고 있는 상품입니다.\n"; + + public static void printStock(StockDto stock) { + System.out.println(WELL_COME_MESSAGE); + System.out.println(stock.stock()); + } + + public static void printErrorMessage(IllegalArgumentException e) { + System.out.println(e.getMessage()); + } + + public static void printReceipt(ReceiptDto receiptDto) { + Map purchaseProducts = receiptDto.purchaseProducts(); + Map presentProducts = receiptDto.presentProducts(); + int membershipDiscountAmount = receiptDto.membershipDiscountAmount(); + + int totalPurchaseQuantity = 0; + int totalPurchasePrice = 0; + int totalPresentPrice = 0; + + + System.out.println("\n============W 편의점============"); + System.out.println("상품명 수량 금액"); + for (Product product : purchaseProducts.keySet()) { + int quantity = purchaseProducts.get(product); + int price = product.getPrice(quantity); + System.out.printf("%-16s%-8s%,d\n", product.getName(), quantity, price); + + totalPurchaseQuantity += quantity; + totalPurchasePrice += price; + } + + System.out.println("============증 정============"); + for (Product product : presentProducts.keySet()) { + int quantity = presentProducts.get(product); + int price = product.getPrice(quantity); + System.out.printf("%-16s%d\n", product.getName(), quantity); + + totalPresentPrice += price; + } + + int finalPrice = totalPurchasePrice - totalPresentPrice - membershipDiscountAmount; + System.out.println("=============================="); + System.out.printf("%-15s%-8s%,d\n", "총구매액", totalPurchaseQuantity, totalPurchasePrice); + System.out.printf("%-23s-%,d\n", "행사할인", totalPresentPrice); + System.out.printf("%-23s-%,d\n", "멤버십할인", membershipDiscountAmount); + System.out.printf("%-24s%,d\n", "내실돈", finalPrice); + } +} diff --git a/src/main/resources/promotions.md b/src/main/resources/promotions.md index d89887ce..6398c046 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,2025-01-01,2025-12-31 +MD추천상품,1,1,2025-01-01,2025-12-31 +반짝할인,1,1,2025-11-01,2025-11-30 diff --git a/src/test/java/store/ApplicationTest.java b/src/test/java/store/ApplicationTest.java index 7eac2290..d89fe652 100644 --- a/src/test/java/store/ApplicationTest.java +++ b/src/test/java/store/ApplicationTest.java @@ -1,6 +1,7 @@ package store; import camp.nextstep.edu.missionutils.test.NsTest; +import java.io.IOException; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -63,6 +64,10 @@ class ApplicationTest extends NsTest { @Override public void runMain() { - Application.main(new String[]{}); + try { + Application.main(new String[]{}); + } catch (IOException e) { + throw new RuntimeException(e); + } } }