├── .gitignore ├── .npmrc ├── images ├── demo_coin.gif ├── test_result.png ├── beverage_icon.png ├── demo_product.gif └── demo_purchase.gif ├── cypress.json ├── index.html ├── package.json ├── LICENSE ├── test └── app.spec.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | -------------------------------------------------------------------------------- /images/demo_coin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-vendingmachine-precourse/HEAD/images/demo_coin.gif -------------------------------------------------------------------------------- /images/test_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-vendingmachine-precourse/HEAD/images/test_result.png -------------------------------------------------------------------------------- /images/beverage_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-vendingmachine-precourse/HEAD/images/beverage_icon.png -------------------------------------------------------------------------------- /images/demo_product.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-vendingmachine-precourse/HEAD/images/demo_product.gif -------------------------------------------------------------------------------- /images/demo_purchase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-vendingmachine-precourse/HEAD/images/demo_purchase.gif -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "test", 3 | "testFiles": "*.spec.js", 4 | "screenshotOnRunFailure": false, 5 | "video": false, 6 | "pluginsFile": false, 7 | "supportFile": false, 8 | "blockHosts": ["cdn.jsdelivr.net"] 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 자판기 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-vendingmachine-precourse", 3 | "version": "1.0.0", 4 | "description": "우아한테크코스 프리코스 자판기 미션", 5 | "dependencies": {}, 6 | "devDependencies": { 7 | "cypress": "8.7.0" 8 | }, 9 | "scripts": { 10 | "test": "cypress run --browser chrome" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/woowacourse/javascript-vendingmachine-precourse.git" 15 | }, 16 | "keywords": [], 17 | "author": "woowacourse", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/woowacourse/javascript-vendingmachine-precourse/issues" 21 | }, 22 | "homepage": "https://github.com/woowacourse/javascript-vendingmachine-precourse#readme", 23 | "engineStrict": true, 24 | "engines": { 25 | "npm": ">=6.0.0", 26 | "node": ">=14.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 woowacourse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/app.spec.js: -------------------------------------------------------------------------------- 1 | describe("구현 결과가 요구사항과 일치해야 한다.", () => { 2 | const baseUrl = "../index.html"; 3 | const SELECTOR = { 4 | COIN_MENU: "#vending-machine-manage-menu", 5 | COIN_CHARGE_INPUT: "#vending-machine-charge-input", 6 | COIN_CHARGE_BUTTON: "#vending-machine-charge-button", 7 | COIN_500: "#vending-machine-coin-500-quantity", 8 | COIN_100: "#vending-machine-coin-100-quantity", 9 | COIN_50: "#vending-machine-coin-50-quantity", 10 | COIN_10: "#vending-machine-coin-10-quantity", 11 | PRODUCT_MENU: "#product-add-menu", 12 | PRODUCT_NAME_INPUT: "#product-name-input", 13 | PRODUCT_PRICE_INPUT: "#product-price-input", 14 | PRODUCT_QUANTITY_INPUT: "#product-quantity-input", 15 | PRODUCT_ADD_BUTTON: "#product-add-button", 16 | PURCHASE_MENU: "#product-purchase-menu", 17 | PURCHASE_CHARGE_INPUT: "#charge-input", 18 | PURCHASE_CHARGE_AMOUNT: "#charge-amount", 19 | PURCHASE_CHARGE_BUTTON: "#charge-button", 20 | PURCHASE_ITEM_BUTTON: ".purchase-button", 21 | PURCHASE_ITEM_QUANTITY: ".product-purchase-quantity", 22 | }; 23 | 24 | before(() => { 25 | Cypress.Commands.add("stubRandomReturns", (returnValues = []) => { 26 | const randomStub = cy.stub(); 27 | 28 | returnValues.forEach((value, index) => { 29 | randomStub.onCall(index).returns(value); 30 | }); 31 | 32 | cy.visit(baseUrl, { 33 | onBeforeLoad: (window) => { 34 | window.MissionUtils = { 35 | Random: { 36 | pickNumberInList: randomStub, 37 | }, 38 | }; 39 | }, 40 | }); 41 | }); 42 | 43 | Cypress.Commands.add("addProduct", (name, price, quantity) => { 44 | cy.get(SELECTOR.PRODUCT_NAME_INPUT).type(name); 45 | cy.get(SELECTOR.PRODUCT_PRICE_INPUT).type(price); 46 | cy.get(SELECTOR.PRODUCT_QUANTITY_INPUT).type(quantity); 47 | cy.get(SELECTOR.PRODUCT_ADD_BUTTON).click(); 48 | }); 49 | }); 50 | 51 | beforeEach(() => { 52 | cy.stubRandomReturns([100, 100, 100, 100, 50]); 53 | }); 54 | 55 | it("상품 1개를 구매할 수 있어야 한다.", () => { 56 | // given 57 | const name = "콜라"; 58 | const price = 1500; 59 | const quantity = 20; 60 | const coinAmount = 450; 61 | const chargeAmount = 3000; 62 | 63 | // 상품 추가 64 | cy.get(SELECTOR.PRODUCT_MENU).click(); 65 | cy.addProduct(name, price, quantity); 66 | cy.addProduct("사이다", 1000, 10); 67 | 68 | // 잔돈 충전 69 | cy.get(SELECTOR.COIN_MENU).click(); 70 | cy.get(SELECTOR.COIN_CHARGE_INPUT).type(coinAmount); 71 | cy.get(SELECTOR.COIN_CHARGE_BUTTON).click(); 72 | 73 | // 금액 투입 74 | cy.get(SELECTOR.PURCHASE_MENU).click(); 75 | cy.get(SELECTOR.PURCHASE_CHARGE_INPUT).type(chargeAmount); 76 | cy.get(SELECTOR.PURCHASE_CHARGE_BUTTON).click(); 77 | 78 | // when 79 | cy.get("[data-product-name='콜라']") 80 | .parent() 81 | .find(SELECTOR.PURCHASE_ITEM_BUTTON) 82 | .click(); 83 | 84 | // then 85 | cy.get(SELECTOR.PURCHASE_CHARGE_AMOUNT).should( 86 | "have.text", 87 | chargeAmount - price 88 | ); 89 | cy.get("[data-product-name='콜라']") 90 | .parent() 91 | .find(SELECTOR.PURCHASE_ITEM_QUANTITY) 92 | .should("have.text", quantity - 1); 93 | cy.get(SELECTOR.COIN_MENU).click(); 94 | cy.get(SELECTOR.COIN_100).should("have.text", "4개"); 95 | cy.get(SELECTOR.COIN_50).should("have.text", "1개"); 96 | }); 97 | 98 | it("잘못된 입력값으로 잔돈 충전을 시도하는 경우 alert이 호출되어야 한다.", () => { 99 | // given 100 | const alertStub = cy.stub(); 101 | const invalidInput = -1; 102 | 103 | cy.on("window:alert", alertStub); 104 | 105 | // when 106 | cy.get(SELECTOR.COIN_MENU).click(); 107 | cy.get(SELECTOR.COIN_CHARGE_INPUT).type(invalidInput); 108 | 109 | // then 110 | cy.get(SELECTOR.COIN_CHARGE_BUTTON) 111 | .click() 112 | .then(() => { 113 | expect(alertStub).to.be.called; 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

자판기

5 | 6 | ## 🔍 진행방식 7 | 8 | - 미션은 **기능 요구사항, 프로그래밍 요구사항, 과제 진행 요구사항** 세 가지로 구성되어 있다. 9 | - 세 개의 요구사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. 10 | - 기능 요구사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. 11 | 12 | 13 | ## 🎯 기능 요구 사항 14 | 반환되는 동전이 최소한이 되는 자판기를 구현한다. 15 | ### 1) 공통 16 | 17 | 상단에 `탭`메뉴가 존재하며 각 탭에 따라 적절한 기능을 수행한다. 18 | 19 | - `상품 관리`탭은 자판기가 보유하고 있는 **상품을 추가**하는 기능을 수행한다. 20 | - `잔돈 충전`탭은 **자판기가 보유할 금액을 충전**하는 기능을 수행한다. 21 | - `상품 구매`탭은 사용자가 **금액을 투입**할 수 있으며, 투입한 금액에 맞춰 **상품을 구매**하고, 남은 금액에 대해서는 **잔돈을 반환**하는 기능을 수행한다. 22 | - 다른 탭으로 이동했다 돌아와도 기존 탭의 상태가 유지되어야 한다. 23 | - localStorage를 이용하여, 새로고침하더라도 가장 최근에 작업한 정보들을 불러올 수 있도록 한다. 24 | 25 | ### 2) 상품 관리 탭 26 | 27 | `상품 관리`탭에서, 다음과 같은 규칙을 바탕으로 상품을 추가한다. 28 | 29 | - 최초 상품 목록은 비워진 상태이다. 30 | - 상품명, 가격, 수량을 입력해 상품을 추가할 수 있다. 31 | - 상품 가격은 100원부터 시작하며, 10원으로 나누어 떨어져야 한다. 32 | - 사용자는 추가한 상품을 확인할 수 있다. 33 | 34 | ### 3) 잔돈 충전 탭 (자판기 보유 동전) 35 | 36 | `잔돈 충전` 탭에서, 다음과 같은 규칙으로 자판기 보유 금액을 충전한다. 37 | 38 | - `잔돈 충전` 탭에서 최초 자판기가 보유한 금액은 0원이며, 각 동전의 개수는 0개이다. 39 | - 잔돈 충전 입력 요소에 충전할 금액을 입력한 후, `충전하기` 버튼을 눌러 자판기 보유 금액을 충전할 수 있다. 40 | - 자판기 보유 금액은 `{금액}원` 형식으로 나타낸다. 41 | - 자판기 보유 금액만큼의 동전이 무작위로 생성된다. 42 | - 동전의 개수는 `{개수}개` 형식으로 나타낸다. 43 | - 자판기 보유 금액을 누적하여 충전할 수 있다. 추가 충전 금액만큼의 동전이 무작위로 생성되어 기존 동전들에 더해진다. 44 | - 상품 구매 탭에서 투입한 금액은 자판기 보유 금액에 더하지 않는다. 45 | 46 | ### 4) 상품 구매 탭 47 | 48 | `상품 구매`탭에서, 다음과 같은 규칙을 바탕으로 금액을 충전하고, 상품을 구매하며, 잔돈을 반환한다. 49 | 50 | - `상품 구매` 페이지에서 최초 충전 금액은 0원이며, 반환된 각 동전의 개수는 0개이다. 51 | - 사용자는 투입할 금액 입력 요소에 투입 금액을 입력한 후, `투입하기`버튼을 이용하여 금액을 투입한다. 52 | - 금액은 10원으로 나누어 떨어지는 금액만 투입할 수 있다. 53 | - 자판기가 보유한 금액은 `{금액}원` 형식으로 나타낸다. 54 | - 금액은 누적으로 투입할 수 있다. 55 | - 품절된 상품의 `구매하기` 버튼은 disabled 되어야 한다. 56 | - 사용자는 `반환하기` 버튼을 통해 잔돈을 반환 받을 수 있다. 57 | 58 | **상품 구매 > 잔돈 계산 모듈** 59 | 60 | `상품 구매` 탭에서 잔돈 반환 시 다음과 같은 규칙을 통해 잔돈을 반환한다. 61 | 62 | - 잔돈을 돌려줄 때는 현재 보유한 최소 개수의 동전으로 잔돈을 돌려준다. 63 | - 지폐를 잔돈으로 반환하는 경우는 없다고 가정한다. 64 | - 잔돈을 반환할 수 없는 경우 잔돈으로 반환할 수 있는 금액만 반환한다. 65 | - 동전의 개수를 나타내는 정보는 `{개수}개` 형식으로 나타낸다. 66 | 67 | --- 68 | 69 | ### 💻 실행 결과 예시 70 | 71 | #### 상품 관리 72 | 73 | 74 | #### 잔돈 충전 75 | 76 | 77 | #### 상품 구매 및 잔돈 반환 78 | 79 | 80 | --- 81 | 82 | ## ✅ 프로그래밍 요구 사항 83 | 84 | ### DOM 선택자 85 | 각 요소에 아래와 같은 선택자를 반드시 지정한다. 86 | 87 | **탭 메뉴 버튼** 88 | 89 | - `상품 구매` 탭으로 이동하는 메뉴 버튼 id는 `product-purchase-menu`이다. 90 | - `잔돈 충전`탭으로 이동하는 메뉴 버튼 id는 `vending-machine-manage-menu`이다. 91 | - `상품 관리`탭으로 이동하는 메뉴 버튼 id는 `product-add-menu`이다. 92 | 93 | **상품 관리(추가) 메뉴** 94 | 95 | - 상품 추가 입력 폼의 상품명 입력 요소의 id는 `product-name-input`이다. 96 | - 상품 추가 입력 폼의 상품 가격 입력 요소의 id는 `product-price-input`이다. 97 | - 상품 추가 입력 폼의 수량 입력 요소의 id는 `product-quantity-input`이다. 98 | - 상품 `추가하기` 버튼 요소의 id는 `product-add-button`이다. 99 | - 추가한 각 상품 요소의 class명은 `product-manage-item`이며, 하위에 아래 요소들을 갖는다. 100 | - 상품명에 해당하는 요소의 class명은 `product-manage-name`이다. 101 | - 가격에 해당하는 요소의 class명은 `product-manage-price`이다. 102 | - 수량에 해당하는 요소의 class명은 `product-manage-quantity`이다. 103 | 104 | **잔돈 충전 (자판기 보유 동전) 메뉴** 105 | 106 | - 자판기가 보유할 금액을 충전할 요소의 id는 `vending-machine-charge-input`이다. 107 | - `충전하기` 버튼에 해당하는 요소의 id는 `vending-machine-charge-button`이다. 108 | - 충전된 금액을 확인하는 요소의 id는 `vending-machine-charge-amount` 이다. 109 | - 보유한 각 동전의 개수에 해당하는 요소의 id는 다음과 같다. 110 | - 500원: `vending-machine-coin-500-quantity` 111 | - 100원: `vending-machine-coin-100-quantity` 112 | - 50원: `vending-machine-coin-50-quantity` 113 | - 10원: `vending-machine-coin-10-quantity` 114 | 115 | **상품 구매 메뉴** 116 | 117 | - 투입 금액 입력 요소의 id는 `charge-input`이다. 118 | - 투입하기 버튼 요소의 id는 `charge-button`이다. 119 | - 투입한 금액을 확인하는 요소의 id는 `charge-amount`이다. 120 | - 반환하기 버튼 요소의 id는 `coin-return-button`이다. 121 | - 반환된 각 동전의 개수에 해당하는 요소의 id는 다음과 같다. 122 | - 500원: `coin-500-quantity` 123 | - 100원: `coin-100-quantity` 124 | - 50원: `coin-50-quantity` 125 | - 10원: `coin-10-quantity` 126 | - 각 상품 요소의 class명은 `product-purchase-item`이고, 하위에 아래 요소들을 갖는다. 127 | - 구매 버튼에 해당하는 요소의 class명은 `purchase-button`이다. 128 | - 상품명에 해당하는 요소의 class명은 `product-purchase-name`이다. 129 | - 가격에 해당하는 요소의 class명은 `product-purchase-price`이다. 130 | - 수량에 해당하는 요소의 class명은 `product-purchase-quantity`이다. 131 | - 상품명은 `dataset` 속성을 사용하고 `data-product-name` 형식으로 저장한다. 132 | - 가격은 `dataset` 속성을 사용하고 `data-product-price` 형식으로 저장한다. 133 | - 수량은 `dataset` 속성을 사용하고 `data-product-quantity` 형식으로 저장한다. 134 | 135 | 136 | --- 137 | 138 | ### 라이브러리 139 | - 잔돈을 무작위로 생성하는 기능은 [`MissionUtils` 라이브러리](https://github.com/woowacourse-projects/javascript-mission-utils#mission-utils)의 `Random.pickNumberInList`를 사용해 구한다. 140 | - `MissionUtils` 라이브러리 스크립트는 `index.html`에 이미 포함되어 전역 객체에 추가되어 있으므로, 따로 `import` 하지 않아도 구현 코드 어디에서든 사용할 수 있다. 141 | 142 | ```javascript 143 | // ex) 144 | const randomNumber = Random.pickNumberInList([10, 50, 100, 500]); 145 | ``` 146 | 147 | --- 148 | 149 | ### 공통 요구사항 150 | 151 | - 스크립트 추가 외에 주어진 `index.html`파일은 수정할 수 없다. 152 | - 스타일(css)은 채점 요소가 아니다. 153 | - 모든 예외 발생 상황은 `alert`메서드를 이용하여 처리한다. 154 | - 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않고, 순수 Vanilla JS로만 구현한다. 155 | - **[자바스크립트 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/feature/styleguide/styleguide/javascript)을 지키면서 프로그래밍** 한다. 156 | - **indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용**한다. 157 | - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. 158 | - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. 159 | - **함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게** 만들어라. 160 | - 변수 선언시 `var` 를 사용하지 않는다. `const` 와 `let` 을 사용한다. 161 | - [const](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/const) 162 | - [let](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/let) 163 | - `import` 문을 이용해 스크립트를 모듈화하고 불러올 수 있게 만든다. 164 | - [https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import) 165 | - **함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.** 166 | - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. 167 | 168 | --- 169 | 170 | ## 📝 과제 진행 요구사항 171 | - 미션은 [javascript-vendingmachine-precourse](https://github.com/woowacourse/javascript-vendingmachine-precourse/) 저장소를 Fork/Clone해 시작한다. 172 | - **기능을 구현하기 전에 javascript-vendingmachine-precourse/docs/README.md 파일에 구현할 기능 목록을 정리**해 추가한다. 173 | - **Git의 커밋 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위**로 추가한다. 174 | - [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 참고해 commit log를 남긴다. 175 | - 과제 진행 및 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고한다. 176 | 177 | ## ✉️ 미션 제출 방법 178 | 179 | - 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. 180 | - GitHub을 활용한 제출 방법은 [프리코스 과제 제출 문서](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 를 참고해 제출한다. 181 | - GitHub에 미션을 제출한 후 [우아한테크코스 지원 플랫폼](https://apply.techcourse.co.kr) 에 접속하여 프리코스 과제를 제출한다. 182 | - 자세한 방법은 [링크](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 를 참고한다. 183 | - **Pull Request만 보내고, 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.** 184 | 185 | 186 | ### 🚨 과제 제출 전 체크리스트 - 0점 방지를 위한 주의사항 187 | - 요구사항에 명시된 출력값 형식을 지키지 않을 경우 기능 구현을 모두 정상적으로 했더라도 0점으로 처리된다. 188 | - 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는 지 확인한다. **테스트가 실패할 경우 0점으로 처리되므로, 반드시 확인 후 제출한다.** 189 | 190 | ### ✔️ 테스트 실행 가이드 191 | - 테스트 실행에 필요한 패키지 설치를 위해 `Node.js` 버전 `14` 이상이 필요하다. 192 | - 다음 명령어를 입력해 패키지를 설치한다. 193 | ```bash 194 | // {폴더 경로}/javascript-vendingmachine-precourse/ 에서 195 | npm install 196 | ``` 197 | 198 | - 설치가 완료되었다면, 다음 명령어를 입력해 테스트를 실행한다. 199 | ```bash 200 | // {폴더 경로}/javascript-vendingmachine-precourse/ 에서 201 | npm run test 202 | ``` 203 | 204 | - 아래와 같은 화면이 나오며 모든 테스트가 pass한다면 성공! 205 | 206 | ![테스트 결과](./images/test_result.png) 207 | --------------------------------------------------------------------------------