diff --git a/README.md b/README.md index d54f268..6fffb1f 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# java-convenience-store-precourse +# java-convenience-stock-precourse + +# 기능 목록 + +1. 확장, 축소 가능한 상품, 프로모션, 재고 +2. 영수증 : 구매 내역 저장 및 계산 +3. 유저 관리 : 유저(멤버십, 영수증 인스턴스 관리), 유저 생성 및 식별 코드 생성 +4. 편의점 초기 설정 기능 : resource 읽고 초기 세팅 +5. 구매 처리 : 재고, 프로모션, 영수증 모델 연관 +6. 입력 view : 테스트 목적으로 inputProvider를 inject +7. 출력 view +8. 편의점 전체 로직 controller +9. 시스템 메세지 enum +10. 에러 메세지 enum +11. 시스템 상수 : 편의점 이름, Y/N문자 설정 \ No newline at end of file diff --git a/src/main/java/store/AppConfig.java b/src/main/java/store/AppConfig.java new file mode 100644 index 0000000..9b73c5e --- /dev/null +++ b/src/main/java/store/AppConfig.java @@ -0,0 +1,119 @@ +package store; + +import java.util.function.Consumer; +import store.controller.StoreController; +import store.model.repository.ProductRepository; +import store.model.repository.PromotionRepository; +import store.model.repository.StockRepository; +import store.model.repository.UserRepository; +import store.service.PurchaseService; +import store.service.RepositoryService; +import store.service.StoreInitializeService; +import store.service.UserService; +import store.utility.ErrorMessage; +import store.view.InputView; +import store.view.OutputView; + +public class AppConfig { + private ProductRepository productRepository; + private PromotionRepository promotionRepository; + private StockRepository stockRepository; + private UserRepository userRepository; + + private RepositoryService repositoryService; + private UserService userService; + private StoreInitializeService storeInitializeService; + private PurchaseService purchaseService; + + private InputView inputView; + private OutputView outputView; + + private StoreController storeController; + + public MockSetting mock; + + public AppConfig(){ + setRepository(); + setService(); + setView(); + setContoller(); + + mock = new MockSetting(this); + } + + public StoreController getStoreController(){ + return storeController; + } + + public static class MockSetting { + public SingleVariable InputView; + public SingleVariable ProductRepository; + public SingleVariable PromotionRepository; + public SingleVariable StockRepository; + + public MockSetting(AppConfig parent){ + this.InputView = new SingleVariable<>(v -> parent.inputView = v, parent::setContoller); + this.ProductRepository = new SingleVariable<>(v -> parent.productRepository = v, ()->{parent.setService(); parent.setContoller();}); + this.StockRepository = new SingleVariable<>(v -> parent.stockRepository = v, ()->{parent.setService(); parent.setContoller();}); + this.PromotionRepository = new SingleVariable<>(v -> parent.promotionRepository = v, ()->{parent.setService(); parent.setContoller();}); + } + + static class SingleVariable implements Injectable { + private final Consumer setter; + private final Refresh func; + public SingleVariable(Consumer setter, Refresh func){ + this.setter = setter; + this.func = func; + } + @Override + public void inject(Object mockedInstance){ + try{ + setter.accept((T) mockedInstance); + func.refresh(); + } catch (Exception e){ + throw new IllegalStateException(ErrorMessage.errorHeader+ + String.format(ErrorMessage.PARAMETER_NOT_ACCEPTABLE.getMessage(), + "입력된 타입의 mockInstance",setter.getClass().getName())); + } + } + } + } + + @FunctionalInterface + interface Injectable { + void inject(Object mockedInstance); + } + + @FunctionalInterface + interface Refresh{ + void refresh(); + } + + private void setRepository(){ + productRepository = new ProductRepository(); + promotionRepository = new PromotionRepository(); + stockRepository = new StockRepository(); + userRepository = new UserRepository(); + } + + private void setService(){ + repositoryService = new RepositoryService(promotionRepository, productRepository, stockRepository); + userService = new UserService(userRepository); + storeInitializeService = new StoreInitializeService(repositoryService); + purchaseService = new PurchaseService(userService, repositoryService); + } + + private void setView(){ + inputView = new InputView(); + outputView = new OutputView(); + } + + private void setContoller(){ + storeController = new StoreController(inputView, outputView, storeInitializeService, purchaseService, repositoryService, userService); + } + + //for debugging +// public void myInputView(){ +// System.out.println("Injected InputView: " + inputView.myIdentity()); +// } +} diff --git a/src/main/java/store/Application.java b/src/main/java/store/Application.java index ec4afd8..6d96b75 100644 --- a/src/main/java/store/Application.java +++ b/src/main/java/store/Application.java @@ -1,7 +1,71 @@ package store; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import store.controller.StoreController; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + AppConfig appConfig = new AppConfig(); + new Application().run(appConfig); + } + + public void run(AppConfig appConfig){ + boolean repeatInput = true; + boolean repeatProgram = true; + StoreController storeController = appConfig.getStoreController(); + String accessingUserID = storeController.initializing(); + + while(repeatProgram){ + storeController.startPurchase(); + while(repeatInput){ + repeatInput = storeController.purchasing(accessingUserID); + } + + repeatInput = true; + + while(repeatInput){ + int result = storeController.endPurchase(accessingUserID); + if(result < 1) repeatInput = false; + if(result < 0) repeatProgram = false; + } + repeatInput = true; + } + storeController.disconnectUser(accessingUserID); + } + + public void testRun(AppConfig appConfig, Breakpoint breakpoint){ + boolean repeatInput = true; + boolean repeatProgram = true; + StoreController storeController = appConfig.getStoreController(); + String accessingUserID = storeController.initializing(); + + while(repeatProgram){ + storeController.startPurchase(); + if(breakpoint==Breakpoint.PRODUCT_GUIDE) break; + while(repeatInput){ + repeatInput = storeController.purchasing(accessingUserID); + if(breakpoint==Breakpoint.FIRST_PURCHASE_ATTEMPT) break; + } + + if(breakpoint==Breakpoint.FIRST_PURCHASE_ATTEMPT) break; + repeatInput = true; + + while(repeatInput){ + int result = storeController.endPurchase(accessingUserID); + if(result < 1) repeatInput = false; + if(result < 0) repeatProgram = false; + } + repeatInput = true; + } + storeController.disconnectUser(accessingUserID); + } + + public enum Breakpoint{ + PRODUCT_GUIDE, + FIRST_PURCHASE_ATTEMPT, + NONE } } diff --git a/src/main/java/store/controller/StoreController.java b/src/main/java/store/controller/StoreController.java new file mode 100644 index 0000000..ae30236 --- /dev/null +++ b/src/main/java/store/controller/StoreController.java @@ -0,0 +1,121 @@ +package store.controller; + +import java.util.List; +import java.util.Objects; +import store.model.domain.Stock; +import store.model.dto.CheckPromotionDTO; +import store.model.dto.CheckPromotionDTO.Type; +import store.service.PurchaseService; +import store.service.RepositoryService; +import store.service.StoreInitializeService; +import store.service.UserService; +import store.utility.ErrorMessage; +import store.utility.InputFormatter; +import store.utility.Parser; +import store.view.InputView; +import store.view.OutputView; + +public class StoreController { + InputView inputView; + OutputView outputView; + StoreInitializeService storeInitializeService; + PurchaseService purchaseService; + RepositoryService repositoryService; + UserService userService; + + public StoreController(InputView inputView, OutputView outputView, + StoreInitializeService storeInitializeService, + PurchaseService purchaseService, + RepositoryService repositoryService, UserService userService){ + this.inputView = inputView; + this.outputView = outputView; + this.storeInitializeService = storeInitializeService; + this.purchaseService = purchaseService; + this.repositoryService = repositoryService; + this.userService = userService; + } + + public String initializing(){ + try{ + storeInitializeService.setPromotions(); + storeInitializeService.setProductsAndStocks(); + return userService.createUser(); + }catch (Exception e){ + outputView.printError(ErrorMessage.errorHeader + e.getMessage()); + throw e; + } + } + + public void startPurchase(){ + outputView.printGreeting(); + List currentStock = repositoryService.getCurrentStockStringList(); + outputView.printCurrentStock(currentStock); + } + + public boolean purchasing(String userKey){ + try{ + String inputLine = inputView.readPurchaseItems(); + String[] splittedInput = InputFormatter.isStockFormat(inputLine); + purchaseEachItem(userKey, splittedInput); + return false; + }catch (Exception e){ + outputView.printError(ErrorMessage.errorHeader + e.getMessage()); + return true; + } + } + + public int endPurchase(String userKey){ + try{ + String inputLine = inputView.readIfMembershipDiscount(); + purchaseService.useMembership(userKey, Objects.equals(inputLine, "Y")); + String receipt = purchaseService.getReceipt(userKey); + outputView.printReceipt(receipt); + purchaseService.resetReceipt(userKey); + inputLine = inputView.readDismissal(); + if(Objects.equals(inputLine, "N")) return -1; + return 0; + }catch (Exception e){ + outputView.printError(ErrorMessage.errorHeader + e.getMessage()); + return 1; + } + } + + public void disconnectUser(String userKey){ + userService.removeUser(userKey); + } + + + + + //------------------------------------------------------------------------------------------------// + // private methods from here + + //정가 질문에 N 답할 시 정가 상품만 구매하지 않는 것으로 이해하고 코드 짰습니다. + private CheckPromotionDTO readAccordingToCheckPromotionDTO(CheckPromotionDTO result, int customerQuantity) { + if (!result.isPurchasable()) throw new IllegalArgumentException(String.format(ErrorMessage.PURCHASE_NOT_ABLE.getMessage(), customerQuantity)); + String productName = result.getProduct().getName(); + if (result.isRelatedStock() && result.isAdditionSuggest()) { + String input = inputView.readIfAdditionalPurchase(productName, result.getInfoQuantity()); + if(Objects.equals(input, "N")){ result.changeQuantity(Type.PROMOTION,-1*result.getInfoQuantity());} + } + if (result.isRelatedStock() && result.getInfoQuantity() != 0) { + String input = inputView.readIfRegularPricePurchase(productName, result.getInfoQuantity()); + if(Objects.equals(input, "N")){ result.changeQuantity(Type.NORMAL,-1*result.getInfoQuantity()); } + } + + repositoryService.deductStock(productName, result.getPromotionQuantity(),result.getNormalQuantity()); + return result; + } + + private void purchaseEachItem(String userKey, String[] splittedInput){ + for(int i=0; i { + String name; + int price; + + //확장성을 위해 이리 복잡하게 설계했습니다. 현재 모델의 필드명의 순서를 바꾸던지 추가해도 + //해당 팩토리 패턴을 그대로 사용할 수 있습니다. + public static Product createProduct(List generatedFields){ + Product product = new Product(); + Field[] fields = Product.class.getDeclaredFields(); + + if (generatedFields == null || generatedFields.size() != fields.length) { + throw new IllegalArgumentException(String.format( + ErrorMessage.RESOURCE_LENGTH_WRONG.getMessage(),fields.length)); + } + + for (int i = 0; i < fields.length; i++) { + fields[i].setAccessible(true); // Allow private field modification + try { + Object value = Parser.castValue(fields[i].getType(), generatedFields.get(i)); + fields[i].set(product, value); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to set field value", e); + } + } + return product; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + @Override + public String toString() { + String processedPrice = String.format("%,d", price); + return name + ' ' + processedPrice + "원"; + } + + @Override + public int compareTo(Product other) { + return this.name.compareTo(other.name); // Sorts alphabetically + } +} 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..94e9bb4 --- /dev/null +++ b/src/main/java/store/model/domain/Promotion.java @@ -0,0 +1,149 @@ +package store.model.domain; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Function; +import java.util.stream.IntStream; +import store.utility.ErrorMessage; +import store.utility.Parser; + +public class Promotion { + private static final int MOST_GET_NUM = 5, LEAST_GET_NUM = 1; + private static final String errorHeader = "Promotion : "; + private static final Rule currentRule = Rule.BUYNGET1; + private final String name; + private final int buy; + private final int get; + private final LocalDateTime start_date; + private final LocalDateTime end_date; + + private Promotion(Builder builder) { + this.name = builder.name; + this.buy = builder.buy; + this.get = builder.get; + this.start_date = builder.start_date; + this.end_date = builder.end_date; + } + + public String getName() { + return name; + } + + public int getBuy() { + return buy; + } + + public int getGet() { + return get; + } + + public boolean isDatePromotionPeriod(LocalDateTime date) { + return start_date.isBefore(date)&&end_date.isAfter(date); + } + + public void isPromotionPeriodsOverlap(Promotion promotion){ + if(this.isDatePromotionPeriod(promotion.start_date) || + this.isDatePromotionPeriod(promotion.end_date)) { + throw new IllegalStateException(ErrorMessage.PROMOTION_DATE_OVERLAP.getMessage()); + } + } + + public static void isMatchingRule(int getNumber){ + if(!currentRule.matches(getNumber)) { + throw new IllegalArgumentException + (String.format(ErrorMessage.RESOURCE_RANGE_WRONG.getMessage(), getNumber, currentRule)); + } + } + + private enum Rule{ + BUYNGET1(1, 1), + BUYNGETN(LEAST_GET_NUM, MOST_GET_NUM); + + private final int minGet, maxGet; + + Rule(int startGetNumber, int endGetNumber){ + this.minGet = startGetNumber; + this.maxGet = endGetNumber; + } + + public boolean matches(int getNumber) { + return this.minGet <=getNumber && getNumber<=this.maxGet; + } + } + + public static class Builder{ + private String name; + private int buy; + private int get; + private LocalDateTime start_date; + private LocalDateTime end_date; + + public static Function, Builder> createBuilderByColumns(List columns){ + return (List values)->{ + Builder builder = new Builder(); + IntStream.range(0, columns.size()) + .forEach(i -> builder.returnBuilderByName(columns.get(i)).apply(values.get(i))); + return builder; + }; + } + + Function returnBuilderByName(String name){ + return switch (name) { + case "name" -> this::setName; + case "buy" -> this::setBuy; + case "get" -> this::setGet; + case "start_date" -> this::setStartDate; + case "end_date" -> this::setEndDate; + default -> throw new IllegalArgumentException( + errorHeader + String.format(ErrorMessage.RESOURCE_COLUMN_WRONG.getMessage(), name)); + }; + } + + Builder setName(String input){ + this.name = input; + return this; + } + + Builder setBuy(String input){ + try{ + this.buy = Parser.NumberParse(input); + return this; + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + Builder setGet(String input){ + try{ + int get = Parser.NumberParse(input); + isMatchingRule(get); + this.get = get; + return this; + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + Builder setStartDate(String input){ + try{ + this.start_date = Parser.DateParse(input); + return this; + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + Builder setEndDate(String input) { + try { + this.end_date = Parser.DateParse(input); + return this; + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + String.format(ErrorMessage.INPUT_NOT_FORMAT.getMessage(),"날짜")); + } + } + + public Promotion build() { + return new Promotion(this); + } + } +} 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..12b0942 --- /dev/null +++ b/src/main/java/store/model/domain/Receipt.java @@ -0,0 +1,111 @@ +package store.model.domain; + +import java.util.TreeMap; +import store.utility.ErrorMessage; +import store.utility.SystemConstantVariable; + +public class Receipt { + private static final String HEADLINE = "===========%s 편의점=============\n" + + "상품명 수량 금액"; + private static final String MIDLINE = "===========증 정=============\n"; + private static final String ENDLINE = "==============================\n"; + + // The Integer in the Map is for total price + // TreeMap는 comparable 또는 comparator를 설정 시 alphabetical 정렬로 저장됨. + private final TreeMap purchasedStocks; + private final TreeMap promotionStocks; + private int totalStockPrice; + private int totalStockQuantity; + private int totalPromotionPrice; + private int totalMembershipPrice; + private int finalPayPrice; + + private Receipt() { + this.purchasedStocks = new TreeMap<>(); + this.promotionStocks = new TreeMap<>(); + } + + // Public method to get the single instance + public static Receipt makeReceipt() { + return new Receipt(); + } + + public void addPurchasedStock(Stock stock){ + purchasedStocks.put(stock, stock.getTotalPrice()); + } + + public void addPromotionStock(Stock stock){ + promotionStocks.put(stock, stock.getTotalPrice()); + } + + public void setTotalStockPrice(){ + totalStockPrice = purchasedStocks.values().stream().reduce(0, Integer::sum); + totalStockQuantity = purchasedStocks.descendingKeySet().stream().mapToInt(Stock::getQuantity).sum(); + } + + public void setTotalPromotionPrice() { + totalPromotionPrice = promotionStocks.values().stream().reduce(0, Integer::sum); + } + + public void setTotalMembershipPrice(int price) { + totalMembershipPrice = price; + } + + public void setFinalPayPrice(){ + finalPayPrice = totalStockPrice - totalPromotionPrice - totalMembershipPrice; + } + + public int getTotalStockPriceWithoutPromotion(){ + int result = totalStockPrice - totalPromotionPrice; + if(result<=0) { + throw new IllegalStateException(String.format(ErrorMessage.ACCESS_BEFORE_INITIALIZATION.getMessage(), "totalStockPrice", "getTotalStockPriceWithoutPromotion")); + } + return result; + } + + @Override + public String toString() { + StringBuilder receiptBuilder = new StringBuilder(); + + generateTopReceipt(receiptBuilder); + generateMidReceipt(receiptBuilder); + generateEndReceipt(receiptBuilder); + + return receiptBuilder.toString(); + } + + private void generateTopReceipt(StringBuilder receiptBuilder){ + receiptBuilder.append(String.format(HEADLINE, SystemConstantVariable.STORE_NAME)); + purchasedStocks.descendingKeySet() + .forEach(stock -> + receiptBuilder.append(stock.getProductName()) + .append(" ") + .append(stock.getQuantity()) + .append(" ") + .append(String.format("%,d", stock.getTotalPrice())) + .append("\n") + ); + } + + private void generateMidReceipt(StringBuilder receiptBuilder){ + receiptBuilder.append(MIDLINE); + promotionStocks.descendingKeySet().forEach(stock-> + receiptBuilder.append(stock.getProductName()) + .append(" ") + .append(stock.getQuantity()) + .append("\n") + ); + } + + private void generateEndReceipt(StringBuilder receiptBuilder){ + receiptBuilder.append(ENDLINE) + .append("총구매액").append(" ").append(totalStockQuantity).append(" ").append(String.format("%,d", totalStockPrice)) + .append("\n") + .append("행사할인").append(" ").append(String.format("-%,d", totalPromotionPrice)) + .append("\n") + .append("멤버십할인").append(" ").append(String.format("-%,d", totalMembershipPrice)) + .append("\n") + .append("내실돈").append(" ").append(String.format("%,d", finalPayPrice)) + .append("\n"); + } +} \ No newline at end of file diff --git a/src/main/java/store/model/domain/Stock.java b/src/main/java/store/model/domain/Stock.java new file mode 100644 index 0000000..b0ef3ed --- /dev/null +++ b/src/main/java/store/model/domain/Stock.java @@ -0,0 +1,93 @@ +package store.model.domain; + +import java.lang.reflect.Field; +import java.util.List; +import store.utility.ErrorMessage; +import store.utility.Parser; + +public class Stock implements Comparable{ + private Product product; + private int quantity; + private Promotion promotion; + + Stock(Product product, Object quantity){ + this.product = product; + this.quantity = (int) quantity; + } + + public Stock setPromotion(Promotion promotion){ + this.promotion = promotion; + return this; + } + + public Stock removePromotion(){ + promotion = null; + return this; + } + + public static Stock createStore(Product product, List storeValues){ + List fields = new java.util.ArrayList<>(List.of(Stock.class.getDeclaredFields())); + fields.removeIf(field -> field.getName().equals("product") || field.getName().equals("promotion")); + if (storeValues == null || storeValues.size() != fields.size()) { + throw new IllegalArgumentException(String.format( + ErrorMessage.RESOURCE_LENGTH_WRONG.getMessage(),fields.size())); + } + return new Stock(product, Parser.castValue(fields.getFirst().getType(), storeValues.getFirst())); + } + + public static Stock createStore(Product product, Promotion promotion, int quantity){ + return new Stock(product, quantity).setPromotion(promotion); + } + + public void deltaQuantity(int delta) { + quantity+=delta; + } + + public void removeQuantity(){ + quantity=0; + } + + public String getProductName() { + return product.getName(); + } + + public Product getProduct(){ + return product; + } + + public Promotion getPromotion() { + return promotion; + } + + public int getTotalPrice(){ + return product.getPrice() * quantity; + } + + public int getQuantity() { + return quantity; + } + + public boolean isEmpty(){ + return product==null&&promotion==null&&quantity==0; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(product.toString()).append(" "); + if(quantity==0){ + builder.append("재고 없음"); + return builder.toString(); + } + builder.append(quantity).append("개").append(" "); + if(promotion != null) { + builder.append(promotion.getName()); + } + return builder.toString(); + } + + @Override + public int compareTo(Stock other) { + return this.product.compareTo(other.product); // Sorts alphabetically + } +} diff --git a/src/main/java/store/model/domain/User.java b/src/main/java/store/model/domain/User.java new file mode 100644 index 0000000..4deee11 --- /dev/null +++ b/src/main/java/store/model/domain/User.java @@ -0,0 +1,69 @@ +package store.model.domain; + +import store.model.domain.Membership.Type; +import store.utility.ErrorMessage; + +public class User { + Receipt receipt; + Membership membership; + boolean useMembership; + boolean receiptAvailable; + + public User(){ + membership = Type.NORMAL.create(); + receipt = Receipt.makeReceipt(); + receiptAvailable = false; + useMembership = false; + } + + public void useMembership(boolean membership) { + this.useMembership = membership; + } + + public void calculateReceipt(){ + receipt.setTotalStockPrice(); + receipt.setTotalPromotionPrice(); + + int membershipDiscount = 0; + if(useMembership) { + membershipDiscount = membership.discount(receipt.getTotalStockPriceWithoutPromotion()); + } + receipt.setTotalMembershipPrice(membershipDiscount); + receipt.setFinalPayPrice(); + + receiptAvailable = true; + } + + public String getReceiptText(){ + if(receiptAvailable){ + return receipt.toString(); + } + throw new IllegalStateException( + String.format(ErrorMessage.ACCESS_BEFORE_INITIALIZATION.getMessage(), + "required receipt's values", "getReceiptText")); + } + + public void resetReceipt(){ + receipt = null; + receipt = Receipt.makeReceipt(); + } + + public ReceiptProxy accessReceipt(){ + return new ReceiptProxy(receipt); + } + + public static class ReceiptProxy { + private final Receipt receipt; + private ReceiptProxy(Receipt receipt) { + this.receipt = receipt; + } + + public void addPurchasedStock(Stock stock){ + receipt.addPurchasedStock(stock); + } + + public void addPromotionStock(Stock stock){ + receipt.addPromotionStock(stock); + } + } +} diff --git a/src/main/java/store/model/dto/CheckPromotionDTO.java b/src/main/java/store/model/dto/CheckPromotionDTO.java new file mode 100644 index 0000000..71913bd --- /dev/null +++ b/src/main/java/store/model/dto/CheckPromotionDTO.java @@ -0,0 +1,63 @@ +package store.model.dto; + +import store.model.domain.Product; +import store.model.domain.Promotion; +import store.model.domain.Stock; + +public class CheckPromotionDTO { + boolean isPurchasable; + boolean isRelatedStock; + Product product; + Promotion promotion; + int infoQuantity; + int normalQuantity; + int promotionQuantity; + boolean isAdditionSuggest; + + + public enum Type{ + NORMAL, + PROMOTION + } + + public CheckPromotionDTO(boolean isPurchasable, boolean isRelatedStock, boolean isAdditionSuggest, + Stock baseStock, int infoQuantity, int normalQuantity, int promotionQuantity){ + this.isPurchasable = isPurchasable; + this.isRelatedStock = isRelatedStock; + this.isAdditionSuggest = isAdditionSuggest; + this.product = baseStock.getProduct(); + this.promotion = baseStock.getPromotion(); + this.infoQuantity = infoQuantity; + this.normalQuantity = normalQuantity; + this.promotionQuantity = promotionQuantity; + } + + public boolean isPurchasable(){ return isPurchasable; } + + public boolean isRelatedStock() { + return isRelatedStock; + } + + public boolean isAdditionSuggest() { + return isAdditionSuggest; + } + + public Product getProduct() { + return product; + } + + public Promotion getPromotion() { return promotion; } + + public int getInfoQuantity() { + return infoQuantity; + } + + public int getNormalQuantity() { return normalQuantity; } + + public int getPromotionQuantity() { return promotionQuantity; } + + public void changeQuantity(Type type, int deltaQuantity) { + if(type==Type.NORMAL) normalQuantity += deltaQuantity; + if(type==Type.PROMOTION) promotionQuantity += deltaQuantity; + } +} diff --git a/src/main/java/store/model/repository/ProductRepository.java b/src/main/java/store/model/repository/ProductRepository.java new file mode 100644 index 0000000..b9129dc --- /dev/null +++ b/src/main/java/store/model/repository/ProductRepository.java @@ -0,0 +1,33 @@ +package store.model.repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import store.model.domain.Product; +import store.model.repository.interfaces.RepositoryProvider; + +public class ProductRepository implements RepositoryProvider { + private final Map productRepository; + private static final String errorHeader = "ProductRepository : "; + + public ProductRepository(){ + productRepository = new HashMap<>(); + } + + @Override + public void addItem(String key, Object item) { + try{ + Product product = (Product) item; + productRepository.putIfAbsent(key, product); + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + @Override + public Product isItem(List generatedFields){ + Product product = Product.createProduct(generatedFields); + addItem(generatedFields.getFirst(), product); + return product; + } +} diff --git a/src/main/java/store/model/repository/PromotionRepository.java b/src/main/java/store/model/repository/PromotionRepository.java new file mode 100644 index 0000000..b51a0f6 --- /dev/null +++ b/src/main/java/store/model/repository/PromotionRepository.java @@ -0,0 +1,65 @@ +package store.model.repository; + +import camp.nextstep.edu.missionutils.DateTimes; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import store.model.domain.Promotion; +import store.model.repository.interfaces.RepositoryProvider; +import store.utility.Pair; + +public class PromotionRepository implements RepositoryProvider { + private final Map> promotionRepository; + private static final String errorHeader = "PromotionRepository : "; + + public PromotionRepository(){ + promotionRepository = new HashMap<>(); + } + + @Override + public void addItem(String key, Object item) { + try{ + Promotion promotion = (Promotion) item; + List newEntry = promotionRepository.get(key); + if(newEntry!=null){ + //validation process + newEntry.forEach(entry -> entry.isPromotionPeriodsOverlap(promotion)); + newEntry.add(promotion); + promotionRepository.replace(key, newEntry); + + }else{ + newEntry= List.of(promotion); + promotionRepository.put(key, newEntry); + } + + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + @Override + public Pair isItem(String name){ + return itemInDateRangeExists(name, DateTimes.now()); + } + + //Boolean defines whether there actually is promotion + public Pair itemInDateRangeExists (String key, LocalDateTime date){ + List registeredPromotions = promotionRepository.get(key); + if(registeredPromotions.isEmpty()) return new Pair<>(false,null); + Optional foundPromotion = registeredPromotions.stream() + .filter(promotion -> promotion.isDatePromotionPeriod(date)).findFirst(); + return new Pair<>(true, foundPromotion.orElse(null)); + } + + //확장을 위해 미리 형태만 잡아둠 + public void removeOldestPromotion(){} + + public void removeAllFinishedPromotions(){} + + @Override + public String toString(){ + return promotionRepository.values().toString(); + } +} diff --git a/src/main/java/store/model/repository/StockRepository.java b/src/main/java/store/model/repository/StockRepository.java new file mode 100644 index 0000000..6d31199 --- /dev/null +++ b/src/main/java/store/model/repository/StockRepository.java @@ -0,0 +1,81 @@ +package store.model.repository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; +import jdk.jfr.Description; +import store.model.domain.Stock; +import store.model.repository.interfaces.RepositoryProvider; +import store.utility.Pair; + +public class StockRepository implements RepositoryProvider { + private final Map> stockRepository; + + public StockRepository(){ + stockRepository = new HashMap<>(); + } + + @Override + @Description("First value : Promotion stock\nSecond Value : Normal Stock") + public Pair isItem(String name){ + return stockRepository.get(name); + } + + @Override + public void addItem(Object item){ + Stock stock = (Stock)item; + String productName = stock.getProductName(); + Pair repositoryValue = isItem(productName); + if(stockRepository.replace(productName, changedStockField(repositoryValue, stock))==null){ + stockRepository.put(productName, changedStockField(repositoryValue, stock)); + } + } + + public void deductItem(String productName, int promotionStockDelta,int normalStockDelta){ + Pair repositoryValue = isItem(productName); + Stock first = repositoryValue.getFirst(); + Stock second = repositoryValue.getSecond(); + if(first!=null) first.deltaQuantity(-1 * promotionStockDelta); + if(second!=null) second.deltaQuantity(-1*normalStockDelta); + } + + public List allEntriesToString() { + return stockRepository.values().stream().flatMap(StockRepository::streamList).toList(); + } + + public static Stream streamList(Pair inputPair){ + return Stream.of(inputPair.getFirst(), inputPair.getSecond()) + .filter(Objects::nonNull) + .map(Object::toString); + } + + + //------------------------------------------------------------------------------------------------// + // private methods from here + + private Pair changedStockField(Pair repositoryValue, Stock newItem){ + if(repositoryValue == null){ repositoryValue = new Pair<>(null, null);} + Stock promotionStock = repositoryValue.getFirst(), + normalStock = repositoryValue.getSecond(); + + if(newItem.getPromotion()!=null){ + repositoryValue.setFirst(processedStock(promotionStock, newItem)); + if(normalStock==null) { + newItem = Stock.createStore(newItem.getProduct(), null, 0); + } + } + repositoryValue.setSecond(processedStock(normalStock, newItem)); + + return repositoryValue; + } + + private Stock processedStock(Stock originalStock, Stock newStock){ + if(originalStock == null) originalStock = newStock; + else originalStock.deltaQuantity(newStock.getQuantity()); + return originalStock; + } + + +} diff --git a/src/main/java/store/model/repository/UserRepository.java b/src/main/java/store/model/repository/UserRepository.java new file mode 100644 index 0000000..ae43a51 --- /dev/null +++ b/src/main/java/store/model/repository/UserRepository.java @@ -0,0 +1,34 @@ +package store.model.repository; + +import java.util.HashMap; +import java.util.Map; +import store.model.domain.User; +import store.model.repository.interfaces.RepositoryProvider; + +public class UserRepository implements RepositoryProvider { + private final Map userRepository; + private static final String errorHeader = "UserRepository : "; + + public UserRepository(){ + userRepository = new HashMap<>(); + } + + @Override + public void addItem(String key, Object item) { + try{ + User user = (User) item; + userRepository.putIfAbsent(key, user); + } catch (Exception e) { + throw new IllegalArgumentException(errorHeader + e.getMessage()); + } + } + + @Override + public User isItem(String userKey){ + return userRepository.get(userKey); + } + + public void removeItem(String userKey){ + userRepository.remove(userKey); + } +} diff --git a/src/main/java/store/model/repository/interfaces/RepositoryProvider.java b/src/main/java/store/model/repository/interfaces/RepositoryProvider.java new file mode 100644 index 0000000..8a4e721 --- /dev/null +++ b/src/main/java/store/model/repository/interfaces/RepositoryProvider.java @@ -0,0 +1,10 @@ +package store.model.repository.interfaces; + +import java.util.List; + +public interface RepositoryProvider { + default void addItem(String key, Object item){}; + default void addItem(Object item){}; + default Object isItem(String name){return null;} + default Object isItem(List generatedFields){return null;} +} diff --git a/src/main/java/store/service/PurchaseService.java b/src/main/java/store/service/PurchaseService.java new file mode 100644 index 0000000..3ec07ca --- /dev/null +++ b/src/main/java/store/service/PurchaseService.java @@ -0,0 +1,108 @@ +package store.service; + +import camp.nextstep.edu.missionutils.DateTimes; +import store.model.domain.User; +import store.model.dto.CheckPromotionDTO; +import store.model.domain.Promotion; +import store.model.domain.Stock; +import store.service.RepositoryService.isItemType; +import store.utility.ErrorMessage; +import store.utility.Pair; + +public class PurchaseService { + UserService userService; + RepositoryService repositoryService; + + public PurchaseService(UserService userService, RepositoryService repositoryService){ + this.userService = userService; + this.repositoryService = repositoryService; + } + + public void confirmPurchaseToReceipt(String userKey, Stock promotionStock, Stock totalStock){ + User user = userService.retrieveUser(userKey); + user.accessReceipt().addPurchasedStock(totalStock); + user.accessReceipt().addPromotionStock(promotionStock); + } + + public void useMembership(String userKey, boolean isMembership){ + userService.retrieveUser(userKey).useMembership(isMembership); + } + + //이건 조건을 다 뺴지 않는 이상 줄이기가 안되드라구요...쩔수없이 이거대로 하기로 + public CheckPromotionDTO checkPromotionForCustomerInput(String productName, int customerQuantity){ + Pair stocks = (Pair) repositoryService.isItem(isItemType.STOCK, productName); + if(stocks == null) throw new IllegalArgumentException(String.format(ErrorMessage.RESOURCE_NO_SUCH_INSTANCE.getMessage(), productName)); + + Stock basePromoStock = stocks.getFirst(); + Stock baseNormStock = stocks.getSecond(); + Promotion promotion; + + if(basePromoStock == null) { promotion = null; } + else { promotion = basePromoStock.getPromotion(); } + + int normalQuantity = baseNormStock.getQuantity(); + + // Case 1: No promotion -> Check only normal stock + if (promotion == null || !promotion.isDatePromotionPeriod(DateTimes.now())) { + return new CheckPromotionDTO(normalQuantity >= customerQuantity, false, false, baseNormStock, 0, customerQuantity, 0); + } + + // Case 2: Promotion applies + int totalMinusPromotion = promotion.getBuy() + promotion.getGet(); + int promotionQuantity = basePromoStock.getQuantity(); + + // Case 2-1: Not enough promo stock → Check normal stock + if (promotionQuantity < totalMinusPromotion) { + return new CheckPromotionDTO(normalQuantity >= customerQuantity, true, false, basePromoStock, customerQuantity, customerQuantity, 0); + } + + // Case 2-2: Customer bought fewer than promo stock → Handle promo suggestion + if (customerQuantity < promotionQuantity) { + int infoQuantity = customerQuantity % totalMinusPromotion; + if (infoQuantity == totalMinusPromotion - 1) { + return new CheckPromotionDTO(true, true, true, basePromoStock, 1, 0, customerQuantity); + } + if (infoQuantity != 0) { + return new CheckPromotionDTO(normalQuantity >= infoQuantity, true, false, basePromoStock, infoQuantity, infoQuantity, customerQuantity - infoQuantity); + } + } + + // Case 2-3: Promotion stock isn't enough for full purchase → Partial promo, rest normal + if (promotionQuantity != totalMinusPromotion && totalMinusPromotion != customerQuantity) { + int infoQuantity = customerQuantity - promotionQuantity + promotionQuantity % totalMinusPromotion; + return new CheckPromotionDTO( + normalQuantity >= infoQuantity && promotionQuantity > customerQuantity - infoQuantity, + true, + false, + basePromoStock, + infoQuantity, + infoQuantity, + customerQuantity - infoQuantity + ); + } + + // Case 2-4: Default fallback when above conditions don't match + return new CheckPromotionDTO( + normalQuantity + promotionQuantity >= customerQuantity, + true, + false, + basePromoStock, + 0, + Math.min(customerQuantity, normalQuantity), + Math.min(customerQuantity, promotionQuantity) + ); + } + + public String getReceipt(String userKey){ + User user = userService.retrieveUser(userKey); + user.calculateReceipt(); + return user.getReceiptText(); + } + + public void resetReceipt(String userKey){ + User user = userService.retrieveUser(userKey); + user.resetReceipt(); + } + + +} diff --git a/src/main/java/store/service/RepositoryService.java b/src/main/java/store/service/RepositoryService.java new file mode 100644 index 0000000..aea060f --- /dev/null +++ b/src/main/java/store/service/RepositoryService.java @@ -0,0 +1,69 @@ +package store.service; + +import java.util.List; +import store.model.repository.ProductRepository; +import store.model.repository.PromotionRepository; +import store.model.repository.interfaces.RepositoryProvider; +import store.model.repository.StockRepository; + +public class RepositoryService { + private final RepositoryProvider promotionRepository; + private final RepositoryProvider productRepository; + private final RepositoryProvider stockRepository; + + public RepositoryService(PromotionRepository promotionRepository, ProductRepository productRepository, StockRepository stockRepository){ + this.promotionRepository = promotionRepository; + this.productRepository = productRepository; + this.stockRepository = stockRepository; + } + + public enum AddItemType { + PROMOTION, + PRODUCT, + @Deprecated + STOCK; + } + + public enum isItemType{ + PROMOTION, + @Deprecated + PRODUCT, + STOCK; + } + + void addItem(AddItemType type, String key, Object item){ + if(type == AddItemType.PROMOTION) promotionRepository.addItem(key, item); + if(type == AddItemType.PRODUCT) productRepository.addItem(key, item); + //if(type == Type.STOCK) : 현재는 사용되지 않으므로 주석처리함 + } + + void addItem(Object item){ + stockRepository.addItem(item); + } + + Object isItem(isItemType type, String name){ + if(type == isItemType.PROMOTION) return promotionRepository.isItem(name); + if(type == isItemType.STOCK) return stockRepository.isItem(name); + //if(type == Type.PRODUCT) : 현재는 사용되지 않으므로 주석처리함 + return null; + } + + Object isItem(List generatedFields){ + return productRepository.isItem(generatedFields); + } + + public List getCurrentStockStringList(){ + StockRepository repository = (StockRepository) stockRepository; + return repository.allEntriesToString(); + } + + public void deductStock(String productName, int promotionStockDelta,int normalStockDelta){ + StockRepository repository = (StockRepository) stockRepository; + repository.deductItem(productName, promotionStockDelta, normalStockDelta); + } + + //디버깅용으로 만들어뒀던 메서드 +// public String getPromotionRepository() { +// return promotionRepository.toString(); +// } +} diff --git a/src/main/java/store/service/StoreInitializeService.java b/src/main/java/store/service/StoreInitializeService.java new file mode 100644 index 0000000..a25f65a --- /dev/null +++ b/src/main/java/store/service/StoreInitializeService.java @@ -0,0 +1,115 @@ +package store.service; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import store.model.domain.Product; +import store.model.domain.Stock; +import store.model.domain.Promotion; +import store.model.domain.Promotion.Builder; +import store.service.RepositoryService.AddItemType; +import store.service.RepositoryService.isItemType; +import store.utility.ErrorMessage; +import store.utility.FileReadTool; +import store.utility.Pair; +import store.utility.Parser; +import java.util.function.Function; + +public class StoreInitializeService { + private final RepositoryService repositoryService; + private List> table; + + public StoreInitializeService(RepositoryService repositoryService){ + this.repositoryService = repositoryService; + } + + public void setPromotions() { + List rawTable = FileReadTool.readFile("promotions.md"); + table = new ArrayList<>(Parser.stringListParse(",", rawTable)); + + Function, Builder> builderRowInjector = Promotion.Builder.createBuilderByColumns(table.getFirst()); + table.removeFirst(); + table.forEach(row -> addPromotion(builderRowInjector.apply(row))); + } + + public void setProductsAndStocks() { + List rawTable = FileReadTool.readFile("products.md"); + table = new ArrayList<>(Parser.stringListParse(",", rawTable)); + + List productFieldIndex = getProductFieldMatchingIndexList(table.getFirst()); + if (productFieldIndex.isEmpty()) { + throw new IllegalArgumentException(ErrorMessage.RESOURCE_COLUMN_WRONG.getMessage()); + } + Integer promotionIndex = getPromotionIndex(table.getFirst()); + table.stream().skip(1).forEach(row -> rowToStock(row, productFieldIndex, promotionIndex)); + } + + //------------------------------------------------------------------------------------------------// + // private methods from here + + private void addPromotion(Builder promotionBuilder) { + Promotion promotion = promotionBuilder.build(); + repositoryService.addItem(AddItemType.PROMOTION, promotion.getName(), promotion); + } + + private void rowToStock(List row, List productFieldIndex, int promotionIndex) { + Product product = extractProduct(row, productFieldIndex); + List storeValues = extractStoreValues(row, productFieldIndex, promotionIndex); + Stock stock = Stock.createStore(product, storeValues); + if(promotionIndex>-1){ + stock.setPromotion(extractPromotion(row, promotionIndex)); + } + repositoryService.addItem(stock); + } + + private Product extractProduct(List row, List productFieldIndex) { + // need to reformat this code if dataset length > 10 + List productValues = new ArrayList<>(Collections.nCopies(productFieldIndex.size(), null)); + for (int i = 0; i < row.size(); i++) { + if (productFieldIndex.contains(i)) { + productValues.set(productFieldIndex.indexOf(i), row.get(i)); + } + } + return (Product) repositoryService.isItem(productValues); + } + + private Promotion extractPromotion(List row, int promotionIndex) { + String value = row.get(promotionIndex); + if(Objects.equals(value, "null")) return null; + Pair promotion = (Pair) repositoryService.isItem(isItemType.PROMOTION, value); + if (!promotion.getFirst()) { + throw new NoSuchElementException( + String.format(ErrorMessage.RESOURCE_NO_SUCH_INSTANCE.getMessage(), value) + ); + } + return promotion.getSecond(); + + } + + private List extractStoreValues(List row, List productFieldIndex, int promotionIndex) { + List storeValues = new ArrayList<>(); + for (int i = 0; i < row.size(); i++) { + if (!productFieldIndex.contains(i) && promotionIndex!=i) { + storeValues.add(row.get(i)); + } + } + return storeValues; + } + + private List getProductFieldMatchingIndexList(List columnLabels) { + Class product = Product.class; + Field[] fields = product.getDeclaredFields(); + return Arrays.stream(fields) + .map(field -> columnLabels.indexOf(field.getName())) + .filter(index -> index >= 0) + .toList(); + } + + private Integer getPromotionIndex(List columnLabels) { + return columnLabels.indexOf("promotion"); + } +} diff --git a/src/main/java/store/service/UserService.java b/src/main/java/store/service/UserService.java new file mode 100644 index 0000000..62ee883 --- /dev/null +++ b/src/main/java/store/service/UserService.java @@ -0,0 +1,29 @@ +package store.service; + +import java.util.UUID; +import store.model.domain.User; +import store.model.repository.UserRepository; + +public class UserService { + UserRepository userRepository; + + public UserService(UserRepository userRepository){ + this.userRepository = userRepository; + } + + public String createUser(){ + User user = new User(); + String userKey = String.valueOf(UUID.randomUUID()); + userRepository.addItem(userKey, user); + return userKey; + } + + public User retrieveUser(String userKey){ + return userRepository.isItem(userKey); + } + + public void removeUser(String userKey){ + userRepository.removeItem(userKey); + } + +} diff --git a/src/main/java/store/utility/ErrorMessage.java b/src/main/java/store/utility/ErrorMessage.java new file mode 100644 index 0000000..f214c8c --- /dev/null +++ b/src/main/java/store/utility/ErrorMessage.java @@ -0,0 +1,24 @@ +package store.utility; + +public enum ErrorMessage { + INPUT_NOT_FORMAT("%s가 아닌 다른 형식의 문자열이 입력되었습니다."), + RESOURCE_COLUMN_WRONG("모델의 필드명과 리소스 내의 컬럼명 %s이 일치하지 않습니다."), + RESOURCE_RANGE_WRONG("리소스 항목 %d은 정해진 규칙 %s의 범위를 초과합니다."), + RESOURCE_LENGTH_WRONG("리소스 항목 리스트 길이가 모델의 필드 길이 %d와 일치하지 않습니다."), + RESOURCE_NO_SUCH_INSTANCE("%s에 해당하는 명칭의 인스턴스가 존재하지 않습니다."), + PROMOTION_DATE_OVERLAP("같은 명칭의 두 프로모션의 날짜가 겹칩니다."), + PARAMETER_NOT_ACCEPTABLE("입력된 %s가 함수의 요구조건 %s에 어긋납니다."), + ACCESS_BEFORE_INITIALIZATION("변수 %s가 초기화 전 함수 %s를 호출했습니다."), + PURCHASE_NOT_ABLE("입력한 상품 개수 %d개는 재고를 초과해 구매할 수 없습니다."),; + + public static final String errorHeader = "[ERROR] "; + private final String message; + + ErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/store/utility/FileReadTool.java b/src/main/java/store/utility/FileReadTool.java new file mode 100644 index 0000000..12fa0f8 --- /dev/null +++ b/src/main/java/store/utility/FileReadTool.java @@ -0,0 +1,27 @@ +package store.utility; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class FileReadTool { + private static final String errorHeader = "FileReadTool : "; + public static List readFile(String fileName){ + List lines = new ArrayList<>(); + try (InputStream inputStream = FileReadTool.class.getClassLoader().getResourceAsStream(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream)))) { + + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } catch (IOException | NullPointerException e) { + throw new RuntimeException("Error reading file: " + fileName, e); + } + return lines; + } +} diff --git a/src/main/java/store/utility/InputFormatter.java b/src/main/java/store/utility/InputFormatter.java new file mode 100644 index 0000000..b8fa194 --- /dev/null +++ b/src/main/java/store/utility/InputFormatter.java @@ -0,0 +1,59 @@ +package store.utility; + +import static store.utility.SystemConstantVariable.INPUT_CONFIRM_STRING; +import static store.utility.SystemConstantVariable.INPUT_REJECT_STRING; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class InputFormatter { + private static final String OUTER_BRACKET = "["; + private static final String INNER_BRACKET = "]"; + private static final String CONNECTOR = "-"; + // Regex of FORMAT rejects 0 or negative value of input's quantity place. + private static final Pattern FULL_FORMAT = Pattern.compile("\\[([^\\]-]+)-([1-9]\\d*)\\](,\\[([^\\]-]+)-([1-9]\\d*)\\])*"); + private static final Pattern PART_FORMAT = Pattern.compile("\\[([^\\]-]+)-([1-9]\\d*)\\]"); + + public static String[] isStockFormat(String inputLine){ + String[] result = patternExtractor(inputLine); + if(result.length>0 && !Objects.equals(result[0], inputLine)) return result; + throw new IllegalArgumentException( + String.format(ErrorMessage.INPUT_NOT_FORMAT.getMessage(), + OUTER_BRACKET+"문자"+CONNECTOR+"양의 정수"+INNER_BRACKET)); + } + + public static String stockFormat(List inputItem){ + if(inputItem.size()%2!=0) { + throw new IllegalArgumentException( + String.format(ErrorMessage.PARAMETER_NOT_ACCEPTABLE.getMessage(), "리스트", "짝수")); + } + StringBuilder resultFormat = new StringBuilder(); + for(int i=0; i matches = new ArrayList<>(); + while (matcher.find()) { + matches.add(matcher.group(1)); + matches.add(matcher.group(2));// Extract full match + } + return matches.toArray(new String[0]); + } + +} diff --git a/src/main/java/store/utility/Pair.java b/src/main/java/store/utility/Pair.java new file mode 100644 index 0000000..7311c1a --- /dev/null +++ b/src/main/java/store/utility/Pair.java @@ -0,0 +1,25 @@ +package store.utility; + +public class Pair { + private K key; + private V value; + + public Pair(K key, V value) { + this.key = key; + this.value = value; + } + + public void setFirst(K key){this.key = key;} + public void setSecond(V value){this.value = value;} + public K getFirst() { return key; } + public V getSecond() { return value; } + + public boolean isEmpty(){ + return key==null&&value==null; + } + + @Override + public String toString() { + return key + " - " + value; + } +} \ No newline at end of file diff --git a/src/main/java/store/utility/Parser.java b/src/main/java/store/utility/Parser.java new file mode 100644 index 0000000..6091e3b --- /dev/null +++ b/src/main/java/store/utility/Parser.java @@ -0,0 +1,38 @@ +package store.utility; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +public final class Parser { + public static List> stringListParse(String regex, List inputList){ + return inputList.stream().map(row-> List.of(row.split(regex))).toList(); + } + + public static List stringParse(String regex, String inputLine){ + return List.of(inputLine.split(regex)); + } + + public static int NumberParse(String inputString){ + try{ + return Integer.parseInt(inputString); + } catch (Exception e) { + throw new IllegalArgumentException(ErrorMessage.INPUT_NOT_FORMAT.getMessage()); + } + } + + public static LocalDateTime DateParse(String inputString){ + try{ + return LocalDate.parse(inputString, DateTimeFormatter.ofPattern("uuuu-MM-dd")).atStartOfDay(); + } catch (Exception e) { + throw new IllegalArgumentException(String.format(ErrorMessage.INPUT_NOT_FORMAT.getMessage(),inputString+" : 날짜")); + } + } + + public static Object castValue(Class type, String value){ + if(type == int.class) return NumberParse(value); + //double, boolean 검사해야하면 추후 여기에 코드 추가 + return value; + } +} diff --git a/src/main/java/store/utility/SystemConstantVariable.java b/src/main/java/store/utility/SystemConstantVariable.java new file mode 100644 index 0000000..14bd4ea --- /dev/null +++ b/src/main/java/store/utility/SystemConstantVariable.java @@ -0,0 +1,12 @@ +package store.utility; + +import java.util.List; + +public final class SystemConstantVariable { + public static final String STORE_NAME = "W"; + //public static final LocalDateTime TODAY = DateTimes.now(); + public static final String INPUT_CONFIRM_STRING = "Y", INPUT_REJECT_STRING ="N"; + public static final List INPUT_EXAMPLES = List.of("사이다", "1", "감자칩", "2"); + + +} diff --git a/src/main/java/store/utility/SystemMessage.java b/src/main/java/store/utility/SystemMessage.java new file mode 100644 index 0000000..8a9d5c5 --- /dev/null +++ b/src/main/java/store/utility/SystemMessage.java @@ -0,0 +1,26 @@ +package store.utility; + +import static store.utility.SystemConstantVariable.INPUT_CONFIRM_STRING; +import static store.utility.SystemConstantVariable.INPUT_REJECT_STRING; + +public enum SystemMessage { + GREETING("안녕하세요. W편의점입니다.\n" + + "현재 보유하고 있는 상품입니다."), + PURCHASE_GUIDE("구매하실 상품명과 수량을 입력해 주세요. (예: %s)"), + NO_PROMOTION_GUIDE("현재 %s %d개는 프로모션 할인이 적용되지 않습니다. 그래도 구매하시겠습니까? (" + + INPUT_CONFIRM_STRING +"/"+ INPUT_REJECT_STRING +")"), + YES_PROMOTION_GUIDE("현재 %s은(는) %d개를 무료로 더 받을 수 있습니다. 추가하시겠습니까? (" + + INPUT_CONFIRM_STRING +"/"+ INPUT_REJECT_STRING +")"), + MEMBERSHIP_GUIDE("멤버십 할인을 받으시겠습니까? (" + INPUT_CONFIRM_STRING +"/"+ INPUT_REJECT_STRING +")"), + DISMISSAL("감사합니다. 구매하고 싶은 다른 상품이 있나요? (" + INPUT_CONFIRM_STRING +"/"+ INPUT_REJECT_STRING +")"); + + private final String message; + + SystemMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/store/view/InputView.java b/src/main/java/store/view/InputView.java new file mode 100644 index 0000000..f28268e --- /dev/null +++ b/src/main/java/store/view/InputView.java @@ -0,0 +1,48 @@ +package store.view; + +import static store.utility.SystemConstantVariable.INPUT_EXAMPLES; +import store.utility.InputFormatter; +import store.utility.SystemMessage; +import store.view.provider.DefaultInputProvider; +import store.view.provider.interfaces.InputProvider; + +public class InputView{ + InputProvider inputProvider; + + public InputView(){ inputProvider = new DefaultInputProvider(); } + public InputView(InputProvider inputProvider){ this.inputProvider = inputProvider;} + + public static InputView createByProvider(InputProvider inputProvider){ + return new InputView(inputProvider); + } + + public String readPurchaseItems(){ + System.out.printf(SystemMessage.PURCHASE_GUIDE.getMessage() + "%n", InputFormatter.stockFormat(INPUT_EXAMPLES)); + return inputProvider.readNormal(); + } + + public String readIfRegularPricePurchase(String productName, int stockAmount){ + System.out.printf(SystemMessage.NO_PROMOTION_GUIDE.getMessage() + "%n", productName, stockAmount); + return inputProvider.readYorN(); + } + + public String readIfAdditionalPurchase(String productName, int stockAmount){ + System.out.printf(SystemMessage.YES_PROMOTION_GUIDE.getMessage() + "%n", productName, stockAmount); + return inputProvider.readYorN(); + } + + public String readIfMembershipDiscount(){ + System.out.println(SystemMessage.MEMBERSHIP_GUIDE.getMessage()); + return inputProvider.readYorN(); + } + + public String readDismissal(){ + System.out.println(SystemMessage.DISMISSAL.getMessage()); + return inputProvider.readYorN(); + } + + //for debugging +// public String myIdentity(){ +// return inputProvider.toString(); +// } +} diff --git a/src/main/java/store/view/OutputView.java b/src/main/java/store/view/OutputView.java new file mode 100644 index 0000000..9670bbe --- /dev/null +++ b/src/main/java/store/view/OutputView.java @@ -0,0 +1,31 @@ +package store.view; + +import java.util.List; +import store.utility.SystemConstantVariable; +import store.utility.SystemMessage; + +public class OutputView { + private static final String BULLETIN = "- "; + + public void printGreeting(){ + System.out.printf(SystemMessage.GREETING.getMessage() + "%n", SystemConstantVariable.STORE_NAME); + lineBreak(); + } + public void printCurrentStock(List currentStock){ + currentStock.forEach(stockLine -> System.out.println(BULLETIN+stockLine)); + lineBreak(); + } + + public void printReceipt(String receipt){ + System.out.printf(receipt); + lineBreak(); + } + + public void printError(String error){ + System.out.println(error); + } + + public void lineBreak(){ + System.out.println("\n"); + } +} diff --git a/src/main/java/store/view/provider/DefaultInputProvider.java b/src/main/java/store/view/provider/DefaultInputProvider.java new file mode 100644 index 0000000..6aa309d --- /dev/null +++ b/src/main/java/store/view/provider/DefaultInputProvider.java @@ -0,0 +1,24 @@ +package store.view.provider; + +import camp.nextstep.edu.missionutils.Console; +import store.utility.InputFormatter; +import store.view.provider.interfaces.InputProvider; + +public class DefaultInputProvider implements InputProvider { + @Override + public String readNormal(){ + return Console.readLine(); + } + + @Override + public String readYorN(){ + String input = Console.readLine(); + return InputFormatter.isConfirmRejectFormat(input); + } + + //for debugging + @Override + public String toString(){ + return "DefaultInput"; + } +} diff --git a/src/main/java/store/view/provider/TestInputProvider.java b/src/main/java/store/view/provider/TestInputProvider.java new file mode 100644 index 0000000..1052fd0 --- /dev/null +++ b/src/main/java/store/view/provider/TestInputProvider.java @@ -0,0 +1,34 @@ +package store.view.provider; + +import java.text.Normalizer; +import store.view.provider.interfaces.InputProvider; + +public class TestInputProvider implements InputProvider { + String purchaseLine; + String[] YorN; + int index; + + public TestInputProvider(String purchaseLine, String[] YorN){ + this.purchaseLine = Normalizer.normalize(purchaseLine, Normalizer.Form.NFC); + this.YorN = YorN; + index = 0; + } + + @Override + public String readNormal(){ + return purchaseLine; + } + + @Override + public String readYorN(){ + if(index == YorN.length) index = 0; + String target = YorN[index++]; + return target; + } + + //for debugging + @Override + public String toString(){ + return "MockInput"; + } +} diff --git a/src/main/java/store/view/provider/interfaces/InputProvider.java b/src/main/java/store/view/provider/interfaces/InputProvider.java new file mode 100644 index 0000000..355df36 --- /dev/null +++ b/src/main/java/store/view/provider/interfaces/InputProvider.java @@ -0,0 +1,6 @@ +package store.view.provider.interfaces; + +public interface InputProvider { + String readNormal(); + String readYorN(); +} diff --git a/src/test/java/store/ApplicationTest.java b/src/test/java/store/ApplicationTest.java index 7eac229..73a8069 100644 --- a/src/test/java/store/ApplicationTest.java +++ b/src/test/java/store/ApplicationTest.java @@ -4,16 +4,24 @@ import org.junit.jupiter.api.Test; import java.time.LocalDate; +import store.Application.Breakpoint; +import store.controller.StoreController; +import store.view.InputView; +import store.view.provider.TestInputProvider; import static camp.nextstep.edu.missionutils.test.Assertions.assertNowTest; import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; class ApplicationTest extends NsTest { + AppConfig appConfig = new AppConfig(); @Test void 파일에_있는_상품_목록_출력() { - assertSimpleTest(() -> { - run("[물-1]", "N", "N"); + assertNowTest(() -> { + String[] YorN = new String[]{"N","N"}; + InputView mockInputView = InputView.createByProvider(new TestInputProvider("[물-1]", YorN)); + appConfig.mock.InputView.inject(mockInputView); + runTest(Breakpoint.PRODUCT_GUIDE); assertThat(output()).contains( "- 콜라 1,000원 10개 탄산2+1", "- 콜라 1,000원 10개", @@ -34,13 +42,16 @@ class ApplicationTest extends NsTest { "- 컵라면 1,700원 1개 MD추천상품", "- 컵라면 1,700원 10개" ); - }); + },LocalDate.of(2024, 11, 2).atStartOfDay()); } @Test void 여러_개의_일반_상품_구매() { assertSimpleTest(() -> { - run("[비타민워터-3],[물-2],[정식도시락-2]", "N", "N"); + String[] YorN = new String[]{"N","N"}; + InputView mockInputView = InputView.createByProvider(new TestInputProvider("[비타민워터-3],[물-2],[정식도시락-2]", YorN)); + appConfig.mock.InputView.inject(mockInputView); + runTest(Breakpoint.NONE); assertThat(output().replaceAll("\\s", "")).contains("내실돈18,300"); }); } @@ -48,7 +59,10 @@ class ApplicationTest extends NsTest { @Test void 기간에_해당하지_않는_프로모션_적용() { assertNowTest(() -> { - run("[감자칩-2]", "N", "N"); + String[] YorN = new String[]{"N","N"}; + InputView mockInputView = InputView.createByProvider(new TestInputProvider("[감자칩-2]", YorN)); + appConfig.mock.InputView.inject(mockInputView); + runTest(Breakpoint.NONE); assertThat(output().replaceAll("\\s", "")).contains("내실돈3,000"); }, LocalDate.of(2024, 2, 1).atStartOfDay()); } @@ -56,8 +70,11 @@ class ApplicationTest extends NsTest { @Test void 예외_테스트() { assertSimpleTest(() -> { - runException("[컵라면-12]", "N", "N"); - assertThat(output()).contains("[ERROR] 재고 수량을 초과하여 구매할 수 없습니다. 다시 입력해 주세요."); + String[] YorN = new String[]{"N","N"}; + InputView mockInputView = InputView.createByProvider(new TestInputProvider("[컵라면-12]", YorN)); + appConfig.mock.InputView.inject(mockInputView); + runTest(Breakpoint.FIRST_PURCHASE_ATTEMPT); + assertThat(output()).contains("[ERROR] 입력한 상품 개수 12개는 재고를 초과해 구매할 수 없습니다."); }); } @@ -65,4 +82,8 @@ class ApplicationTest extends NsTest { public void runMain() { Application.main(new String[]{}); } + + private void runTest(Breakpoint breakpoint){ + new Application().testRun(appConfig, breakpoint); + } }