diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000..bf20b889
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,31 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "es6": true
+ },
+ "extends": "airbnb-base",
+ "overrides": [
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ "semi": ["error", "always"],
+ "quotes": ["error", "single"],
+ "indent": ["error", 4],
+ "no-return-assign": "off",
+ "no-param-reassign": "off",
+ "arrow-parens": "off",
+ "import/prefer-default-export": "off",
+ "class-methods-use-this": "off",
+ "comma-dangle": "off",
+ "no-unused-vars": "off",
+ "import/extensions": "off",
+ "max-classes-per-file": "off",
+ "no-alert": "off",
+ "no-unsafe-negation": "off",
+ "consistent-return": "off"
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..ce2de85d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,105 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..b58b603f
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..a55e7a17
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/js-vending-machine.iml b/.idea/js-vending-machine.iml
new file mode 100644
index 00000000..0c8867d7
--- /dev/null
+++ b/.idea/js-vending-machine.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..56b41604
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cypress.config.js b/cypress.config.js
new file mode 100644
index 00000000..0969aae3
--- /dev/null
+++ b/cypress.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ e2e: {
+ setupNodeEvents(on, config) {
+ // implement node event listeners here
+ },
+ },
+};
diff --git a/cypress/e2e/home.cy.js b/cypress/e2e/home.cy.js
new file mode 100644
index 00000000..cd669e18
--- /dev/null
+++ b/cypress/e2e/home.cy.js
@@ -0,0 +1,27 @@
+describe('사이트 홈 테스트', () => {
+ beforeEach('페이지 방문', () => {
+ cy.visit('/');
+ });
+
+ homeSpec();
+})
+
+function homeSpec() {
+ it('첫 페이지에 상품관리, 잔돈충전, 상품구매 버튼이 있다.', () => {
+ cy.get('#stock-manage-menu').should('exist');
+ cy.get('#vending-machine-manage-menu').should('exist');
+ cy.get('#product-purchase-menu').should('exist');
+ cy.get('.stock-container').should('not.be.visible');
+ });
+
+ it('상품관리탭을 클릭하면 상품 추가하기 창이 보여진다.', () => {
+ cy.clickStockTab();
+ cy.get('.stock-container').should('be.visible');
+ });
+
+ it('잔돈충전탭을 클릭하면 잔돈 충전하기 창이 보여진다.', () => {
+ cy.clickRechargeTab();
+ cy.get('.recharge-container').should('be.visible');
+ });
+}
+
diff --git a/cypress/e2e/recharge.cy.js b/cypress/e2e/recharge.cy.js
new file mode 100644
index 00000000..b991c418
--- /dev/null
+++ b/cypress/e2e/recharge.cy.js
@@ -0,0 +1,80 @@
+import { ERROR_MESSAGE } from "../../src/js/common/error.js";
+describe('잔돈 충전하기 테스트', () => {
+ beforeEach('페이지 방문', () => {
+ cy.visit('/');
+ });
+
+ beforeEach('잔돈충전 탭 클릭', () => {
+ cy.clickRechargeTab();
+ });
+
+ rechargeSpeck();
+})
+
+function rechargeSpeck() {
+ const amount = 100;
+
+ it('최초 자판기의 보유금액은 0원이다.', () => {
+ cy.get('#recharge-amount').should('have.text', 0);
+ });
+
+ it('최소 충전 금액은 100원이다. :: 경계값', () => {
+ cy.typeRechargeAmount(99);
+ checkAlert(cy.clickRecharge(), ERROR_MESSAGE.InputMinInsufficientError);
+ });
+
+ it('충전 금액의 단위는 10원이다.', () => {
+ cy.typeRechargeAmount(101);
+ checkAlert(cy.clickRecharge(), ERROR_MESSAGE.InputPriceUnitError);
+ });
+
+ it('자판기에 금액을 입력하여 충전하기 버튼을 누르면 보유 금액을 충전할 수 있다.', () => {
+ cy.typeRechargeAmount(amount);
+ cy.clickRecharge();
+ cy.get('#recharge-amount').should('have.text', amount);
+ });
+
+ it('금액을 충전하면 잔돈이 생성된다.', () => {
+ cy.typeRechargeAmount(amount);
+ cy.clickRecharge();
+ cy.get('#recharge-cashbox-container').children().should('have.length', 4);
+ });
+
+ it('보유 금액은 누적된다.', () => {
+ const newAmount = 200;
+ cy.typeRechargeAmount(amount);
+ cy.clickRecharge();
+ cy.get('#recharge-amount').should('have.text', amount);
+
+ cy.get('#recharge-input').clear();
+ cy.typeRechargeAmount(newAmount);
+ cy.clickRecharge();
+ cy.get('#recharge-amount').should('have.text', amount + newAmount);
+ });
+
+ it('다른 탭을 클릭해도 보유된 금액은 유지된다.', () => {
+ cy.typeRechargeAmount(amount);
+ cy.clickRecharge();
+ cy.get('#recharge-amount').should('have.text', amount);
+
+ cy.clickStockTab();
+ cy.clickRechargeTab();
+ cy.get('#recharge-amount').should('have.text', amount);
+ });
+
+ it('같은 세션에서 새로고침을 해도 보유 금액과 잔돈은 저장된다.', () => {
+ cy.typeRechargeAmount(amount);
+ cy.clickRecharge();
+ cy.get('#recharge-amount').should('have.text', amount);
+
+ cy.reload();
+ cy.clickRechargeTab();
+ cy.get('#recharge-amount').should('have.text', amount);
+ });
+}
+
+function checkAlert(scenario, message) {
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+ scenario.then(() => expect(stub.getCall(0)?.lastArg).to.equals(message));
+}
diff --git a/cypress/e2e/stock.cy.js b/cypress/e2e/stock.cy.js
new file mode 100644
index 00000000..5e64e98f
--- /dev/null
+++ b/cypress/e2e/stock.cy.js
@@ -0,0 +1,108 @@
+import { ERROR_MESSAGE } from "../../src/js/common/error.js";
+describe('상 관리 테스트', () => {
+ beforeEach('페이지 방문', () => {
+ cy.visit('/');
+ });
+
+ beforeEach('상품관리 탭 클릭', () => {
+ cy.clickStockTab();
+ });
+
+ stockSpec();
+})
+
+function stockSpec() {
+ const name = '콜라';
+ const price = 100;
+ const quantity = 1;
+
+ it('상품명, 가격, 수량는 필수로 입력해야 한다. :: 전부 미기입', () => {
+ checkAlert(cy.clickStockAdd(), ERROR_MESSAGE.InputRequiredStock);
+ });
+
+ it('상품명, 가격, 수량는 필수로 입력해야 한다. :: 가격 미기입', () => {
+ cy.typename(name);
+ cy.typeStockQuantity(quantity);
+ checkAlert(cy.clickStockAdd(), ERROR_MESSAGE.InputRequiredStock);
+ });
+
+ it('가격은 100원 이상 입력해야 한다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(10);
+ cy.typeStockQuantity(quantity);
+ checkAlert(cy.clickStockAdd(), ERROR_MESSAGE.InputMinInsufficientError);
+ });
+
+ it('가격은 10원 단위로 입력해야 한다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(1001);
+ cy.typeStockQuantity(quantity);
+ checkAlert(cy.clickStockAdd(), ERROR_MESSAGE.InputPriceUnitError);
+ });
+
+ it('수량은 1개 이상 입력해야 한다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(price);
+ cy.typeStockQuantity(0);
+ checkAlert(cy.clickStockAdd(), ERROR_MESSAGE.InputMinQuantityError);
+ });
+
+ it('상품을 추가하면 상품 목록에 추가 되어야 한다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(price);
+ cy.typeStockQuantity(quantity);
+ cy.clickStockAdd();
+ cy.get('#stock-inventory-container').children(`.${name}`).should('exist');
+ });
+
+ it('같은 상품을 추가하면 상품 목록에 새 정보로 갱신 되어야 한다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(price);
+ cy.typeStockQuantity(quantity);
+ cy.clickStockAdd();
+
+ cy.get('#stock-name-input').clear();
+ cy.get('#stock-price-input').clear();
+ cy.get('#stock-quantity-input').clear();
+
+ cy.typename(name);
+ cy.typeStockPrice(200);
+ cy.typeStockQuantity(quantity);
+ cy.clickStockAdd();
+
+ cy.get('#stock-inventory-container')
+ .children(`.${name}`)
+ .children('td').eq(1).should('have.text', 200);
+ });
+
+ it('다른 탭을 클릭하더라도 추가된 상품은 저장된다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(price);
+ cy.typeStockQuantity(quantity);
+ cy.clickStockAdd();
+ cy.get('#stock-inventory-container').children(`.${name}`).should('exist');
+
+ cy.clickRechargeTab();
+ cy.clickStockTab();
+ cy.get('#stock-inventory-container').children(`.${name}`).should('exist');
+ });
+
+
+ it('같은 세션에서 새로고침을 해도 추가한 상품은 저장된다.', () => {
+ cy.typename(name);
+ cy.typeStockPrice(price);
+ cy.typeStockQuantity(quantity);
+ cy.clickStockAdd();
+ cy.get('#stock-inventory-container').children(`.${name}`).should('exist');
+
+ cy.reload();
+ cy.clickStockTab();
+ cy.get('#stock-inventory-container').children(`.${name}`).should('exist');
+ });
+}
+
+function checkAlert(scenario, message) {
+ const stub = cy.stub();
+ cy.on('window:alert', stub);
+ scenario.then(() => expect(stub.getCall(0)?.lastArg).to.equals(message));
+}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 00000000..02e42543
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 00000000..29e2a30a
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,40 @@
+// Stock
+Cypress.Commands.add('clickStockTab', () => {
+ cy.get('#stock-manage-menu').click();
+})
+Cypress.Commands.add('clickStockAdd', () => {
+ cy.get('#stock-add-button').click();
+})
+
+Cypress.Commands.add('typename', (value) => {
+ cy.get('#stock-name-input').type(value);
+})
+
+Cypress.Commands.add('typeStockPrice', (value) => {
+ cy.get('#stock-price-input').type(value);
+})
+
+Cypress.Commands.add('typeStockQuantity', (value) => {
+ cy.get('#stock-quantity-input').type(value);
+})
+
+// Recharge
+Cypress.Commands.add('clickRechargeTab', () => {
+ cy.get('#vending-machine-manage-menu').click();
+})
+
+Cypress.Commands.add('clickRecharge', (value) => {
+ cy.get('#recharge-button').click();
+})
+
+Cypress.Commands.add('typeRechargeAmount', (value) => {
+ cy.get('#recharge-input').type(value);
+})
+
+Cypress.Commands.add('getRechargeCoinName', (i) => {
+ return +cy.get('#recharge-cashbox-container').children('tr').eq(i).children('td').eq(i).invoke('text');
+})
+
+Cypress.Commands.add('getRechargeCoinQuantity', (i) => {
+ return +cy.get('#recharge-cashbox-container').children('tr').eq(i).children('td').eq(i + 1).invoke('text');
+})
\ No newline at end of file
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js
new file mode 100644
index 00000000..0e7290a1
--- /dev/null
+++ b/cypress/support/e2e.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/e2e.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
\ No newline at end of file
diff --git a/index.css b/index.css
index f585b82b..0800bf2c 100644
--- a/index.css
+++ b/index.css
@@ -1,7 +1,7 @@
.purchase-available,
-.product-inventory,
+.stock-inventory,
.cashbox-change,
-.cashbox-remaining {
+.recharge-cashbox-table {
border-collapse: collapse;
border-spacing: 0;
text-align: center;
@@ -10,12 +10,12 @@
}
.cashbox-change,
-.cashbox-remaining {
+.recharge-cashbox-table {
width: 100%;
max-width: 300px;
}
-.product-inventory {
+.stock-inventory {
width: 340px;
}
@@ -24,14 +24,14 @@
}
.cashbox-change col,
-.cashbox-remaining col {
+.recharge-cashbox-table col {
width: 65px;
}
.purchase-available td,
-.product-inventory td,
+.stock-inventory td,
.cashbox-change td,
-.cashbox-remaining td {
+.recharge-cashbox-table td {
border-color: black;
border-style: solid;
border-width: 1px;
@@ -43,9 +43,9 @@
}
.purchase-available th,
-.product-inventory th,
+.stock-inventory th,
.cashbox-change th,
-.cashbox-remaining th {
+.recharge-cashbox-table th {
border-color: black;
border-style: solid;
border-width: 1px;
@@ -100,8 +100,8 @@ input[type='text'] {
}
#charge-button,
-#product-add-button,
-#vending-machine-charge-button {
+#stock-add-button,
+#recharge-button {
padding: 0.5rem;
margin-left: 0.5rem;
}
@@ -131,19 +131,19 @@ section p {
flex-direction: column;
}
-.product-inventory {
+.stock-inventory {
margin-top: 1rem;
}
-.product-container input:not(:first-child) {
+.stock-inputs input:not(:first-child) {
margin-top: 0.5rem;
}
-.vending-machine-wrapper {
+.recharge-wrapper {
width: 65%;
}
-#product-add-button {
+#stock-add-button {
margin-left: 0;
margin-top: 0.5rem;
width: 78%;
@@ -157,10 +157,10 @@ button:active {
box-shadow: 1px 1px 2px 1px #d8d8d8 inset;
}
-.cashbox-remaining,
+.recharge-cashbox-table,
.purchase-available,
.cashbox-change,
-.product-inventory {
+.stock-inventory {
border-radius: 10px;
border-collapse: collapse;
border-style: hidden;
@@ -168,12 +168,19 @@ button:active {
}
.purchase-available td,
-.product-inventory td,
+.stock-inventory td,
.cashbox-change td,
-.cashbox-remaining td,
+.recharge-cashbox-table td,
.purchase-available th,
-.product-inventory th,
+.stock-inventory th,
.cashbox-change th,
-.cashbox-remaining th {
+.recharge-cashbox-table th {
border: 1px solid rgb(66, 66, 66);
}
+
+.stock-container,
+.recharge-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/index.html b/index.html
index 03468a46..f735be38 100644
--- a/index.html
+++ b/index.html
@@ -9,10 +9,11 @@
-
+
+