diff --git a/.exlintrc.json b/.exlintrc.json
new file mode 100644
index 0000000..d3f0fb1
--- /dev/null
+++ b/.exlintrc.json
@@ -0,0 +1,23 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true,
+ "cypress/globals": true
+ },
+ "extends": ["airbnb-base", "plugin:cypress/recommended"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+ "plugins": ["@typescript-eslint", "cypress"],
+ "ignorePatterns": ["dist/", "node_modules/"],
+ "rules": {
+ "cypress/no-assigning-return-values": "error",
+ "cypress/no-unnecessary-waiting": "error",
+ "cypress/assertion-before-screenshot": "warn",
+ "cypress/no-force": "warn",
+ "cypress/no-async-tests": "error",
+ "import/extensions": "always"
+ }
+ }
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a1066ba..e7a7849 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
+package-lock.json
+yarn.lock
+cypress/fixtures/
+cypress/intergration/examples/
+index.html
+
# Created by https://www.toptal.com/developers/gitignore/api/vscode,node,intellij
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,node,intellij
@@ -249,7 +255,7 @@ typings/
# Nuxt.js build / generate output
.nuxt
-dist
+# dist
# Gatsby files
.cache/
diff --git a/.prettierc.json b/.prettierc.json
new file mode 100644
index 0000000..4610753
--- /dev/null
+++ b/.prettierc.json
@@ -0,0 +1,5 @@
+{
+ "parser": "typescript",
+ "singleQuote": true,
+ "trailingComma": "all"
+ }
diff --git a/README.md b/README.md
index 4d8f0b5..0c12841 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,9 @@
+- **데모페이지**
+ https://jwon42.github.io/javascript-calculator/
+
+- **PR**
+ https://github.com/transcendence42/javascript-calculator/pull/4
+
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/cypress/integration/calculator.spec.js b/cypress/integration/calculator.spec.js
new file mode 100644
index 0000000..4fa7c2c
--- /dev/null
+++ b/cypress/integration/calculator.spec.js
@@ -0,0 +1,79 @@
+const calculate = (number1, operator, number2, expectedResult) => {
+ cy.get('.modifiers').click();
+ for (let index = 0 ; index < number1.length ; index += 1) {
+ cy.get('.digit').contains(number1[index]).click();
+ }
+ cy.get('.operations').contains(operator).click();
+ for (let index = 0 ; index < number2.length ; index += 1) {
+ cy.get('.digit').contains(number2[index]).click();
+ }
+ cy.get('.operations').contains('=').click();
+ cy.get('#total').should('have.text', expectedResult)
+}
+
+describe('계산기 테스트', () => {
+ beforeEach(() => {
+ cy.visit('/');
+ });
+
+ it('1. 숫자 입력 시 3자리까지만 결과창에 표시', () => {
+ cy.get('.digit').contains('1').click();
+ cy.get('#total').should('have.text', '1');
+ cy.get('.digit').contains('2').click();
+ cy.get('#total').should('have.text', '12');
+ cy.get('.digit').contains('3').click();
+ cy.get('#total').should('have.text', '123');
+ cy.get('.digit').contains('4').click();
+ cy.get('#total').should('have.text', '123');
+ cy.get('.digit').contains('5').click();
+ cy.get('#total').should('have.text', '123');
+ });
+
+ it('2. [AC] 입력 시 0으로 초기화', () => {
+ cy.get('.digit').contains('4').click();
+ cy.get('.digit').contains('2').click();
+ cy.get('.modifiers').click();
+ cy.get('#total').should('have.text', '0');
+ })
+
+ it('3. 연산자는 1개만 입력 가능', () => {
+ cy.get('.digit').contains('4').click();
+ cy.get('.digit').contains('2').click();
+ cy.get('.operations').contains('+').click();
+ cy.get('.operations').contains('-').click();
+ cy.get('#total').should('have.text', '42+');
+ cy.get('.modifiers').click();
+ })
+
+ it('4. [숫자][연산자][숫자] 포맷 이후 연산자가 올 수 없음', () => {
+ cy.get('.digit').contains('4').click();
+ cy.get('.digit').contains('2').click();
+ cy.get('.operations').contains('+').click();
+ cy.get('.digit').contains('4').click();
+ cy.get('.digit').contains('2').click();
+ cy.get('.operations').contains('-').click();
+ cy.get('#total').should('have.text', '42+42');
+ cy.get('.modifiers').click();
+ })
+
+ it('5. 한 자릿수 사칙연산', () => {
+ calculate([4], '+', [2], '6');
+ calculate([4], '-', [2], '2');
+ calculate([4], 'X', [2], '8');
+ calculate([4], '/', [2], '2');
+ })
+
+ it('6. 두 자릿수 사칙연산', () => {
+ calculate([4, 2], '+', [2, 4], '66');
+ calculate([4, 2], '-', [2, 4], '18');
+ calculate([4, 2], 'X', [2, 4], '1008');
+ calculate([4, 2], '/', [2, 4], '1');
+ })
+
+ it('7. 세 자릿수 사칙연산', () => {
+ calculate([1, 2, 3], '+', [3, 2, 1], '444');
+ calculate([1, 2, 3], '-', [3, 2, 1], '-198');
+ calculate([1, 2, 3], 'X', [3, 2, 1], '39483');
+ calculate([1, 2, 3], '/', [3, 2, 1], '0');
+ })
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..59b2bab
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..119ab03
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..d68db96
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.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')
diff --git a/dist/css/index.css b/dist/css/index.css
new file mode 100644
index 0000000..4e6a281
--- /dev/null
+++ b/dist/css/index.css
@@ -0,0 +1,87 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+ font-family: sans-serif;
+}
+
+#app {
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.calculator {
+ width: 300px;
+ display: grid;
+ grid-template-areas:
+ 'total total total total'
+ 'modif modif modif oper'
+ 'digit digit digit oper'
+ 'digit digit digit oper'
+ 'digit digit digit oper'
+ 'digit digit digit oper';
+ grid-auto-columns: 1fr;
+ grid-auto-rows: 1fr;
+ height: 500px;
+}
+
+button {
+ font-size: 2rem;
+ border: 0.5px solid #98999b;
+}
+
+.modifiers button {
+ background-color: #ccc;
+}
+
+.operations button {
+ background-color: orange;
+}
+
+.digits button {
+ background-color: #efefef;
+}
+
+#total {
+ grid-area: total;
+ background-color: #333;
+ color: white;
+ margin: 0;
+ padding: 1rem;
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-end;
+ font-size: 4rem;
+}
+
+.digits {
+ grid-area: digit;
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row-reverse;
+}
+
+.digits button {
+ flex: 1 0 30%;
+}
+
+.digit.wide {
+ flex: 2 0 60%;
+ order: 1;
+}
+
+.modifiers {
+ grid-area: modif;
+ grid-auto-flow: column;
+ grid-auto-columns: 1fr;
+}
+
+.operations {
+ grid-area: oper;
+}
+
+.subgrid {
+ display: grid;
+}
diff --git a/dist/images/calculator_icon.png b/dist/images/calculator_icon.png
new file mode 100644
index 0000000..e91b9ef
Binary files /dev/null and b/dist/images/calculator_icon.png differ
diff --git a/dist/images/calculator_ui.png b/dist/images/calculator_ui.png
new file mode 100644
index 0000000..05990eb
Binary files /dev/null and b/dist/images/calculator_ui.png differ
diff --git a/dist/ts/constants/index.js b/dist/ts/constants/index.js
new file mode 100644
index 0000000..bef1811
--- /dev/null
+++ b/dist/ts/constants/index.js
@@ -0,0 +1,13 @@
+export const OPERATOR = {
+ ADD: "+",
+ SUBSTRACT: "-",
+ MULTIPLY: "X",
+ DIVIDE: "/",
+ EQUAL: "=",
+};
+export const EQUAL = '=';
+export const REGEXP = {
+ // SIGN: '\\-{1}',
+ NUMBERS: '\\-?\\d{1,3}',
+ OPERATORS: 'X|\\-|\\+|\\/{1}'
+};
diff --git a/dist/ts/controller/event.js b/dist/ts/controller/event.js
new file mode 100644
index 0000000..54e5851
--- /dev/null
+++ b/dist/ts/controller/event.js
@@ -0,0 +1,18 @@
+import { pushTotalValue, clearTotalValue } from "../view/index.js";
+export const setEventListener = () => {
+ document
+ .getElementsByClassName("digits")[0]
+ .addEventListener("click", (e) => {
+ pushTotalValue(e);
+ });
+ document
+ .getElementsByClassName("operations")[0]
+ .addEventListener("click", (e) => {
+ pushTotalValue(e);
+ });
+ document
+ .getElementsByClassName("modifier")[0]
+ .addEventListener("click", () => {
+ clearTotalValue();
+ });
+};
diff --git a/dist/ts/controller/index.js b/dist/ts/controller/index.js
new file mode 100644
index 0000000..6abe59a
--- /dev/null
+++ b/dist/ts/controller/index.js
@@ -0,0 +1,4 @@
+import { setEventListener } from "./event.js";
+export const controller = () => {
+ setEventListener();
+};
diff --git a/dist/ts/controller/parser.js b/dist/ts/controller/parser.js
new file mode 100644
index 0000000..efe3557
--- /dev/null
+++ b/dist/ts/controller/parser.js
@@ -0,0 +1,13 @@
+export const parseTotalValue = (totalValue) => {
+ let isFirstNumberMinus = false;
+ if (totalValue[0] === "-") {
+ totalValue = totalValue.slice(1, totalValue.length);
+ isFirstNumberMinus = true;
+ }
+ const operator = totalValue.replace(/\d/g, "");
+ const numbers = totalValue.split(operator);
+ if (isFirstNumberMinus) {
+ return ["-" + numbers[0], operator, numbers[1]];
+ }
+ return [numbers[0], operator, numbers[1]];
+};
diff --git a/dist/ts/controller/validator.js b/dist/ts/controller/validator.js
new file mode 100644
index 0000000..63dc539
--- /dev/null
+++ b/dist/ts/controller/validator.js
@@ -0,0 +1,36 @@
+import { REGEXP, EQUAL } from "../constants/index.js";
+export const isNumber = (str) => {
+ return /^[\d.]+(?:e-?\d+)?$/.test(str);
+};
+export const checkFirstTotalValue = (totalValue, clickValue) => {
+ return totalValue === "0" && (isNumber(clickValue) || clickValue === "-");
+};
+export const checkPreventClickValue = (totalValue, clickValue) => {
+ const tmpValue = totalValue + clickValue;
+ const tmpValueArray = tmpValue.match("(" +
+ REGEXP.NUMBERS +
+ ")?(" +
+ REGEXP.OPERATORS +
+ ")?(" +
+ REGEXP.NUMBERS +
+ ")?");
+ if (tmpValueArray) {
+ if (!tmpValueArray[2] && tmpValueArray[3]) {
+ return true;
+ }
+ if (tmpValueArray[2] &&
+ !isNumber(totalValue.charAt(totalValue.length - 1)) &&
+ !isNumber(clickValue)) {
+ return true;
+ }
+ if (tmpValueArray[3] && !isNumber(clickValue) && clickValue !== EQUAL) {
+ return true;
+ }
+ if (tmpValueArray[3] &&
+ tmpValue.length !== tmpValueArray[0].length &&
+ clickValue !== EQUAL) {
+ return true;
+ }
+ }
+ return false;
+};
diff --git a/dist/ts/index.js b/dist/ts/index.js
new file mode 100644
index 0000000..401b580
--- /dev/null
+++ b/dist/ts/index.js
@@ -0,0 +1,5 @@
+import { controller } from "./controller/index.js";
+const app = () => {
+ controller();
+};
+app();
diff --git a/dist/ts/model/calculator.js b/dist/ts/model/calculator.js
new file mode 100644
index 0000000..ee68350
--- /dev/null
+++ b/dist/ts/model/calculator.js
@@ -0,0 +1,29 @@
+import { OPERATOR, REGEXP } from "../constants/index.js";
+export const calculator = (splitedTotalValue) => {
+ if (splitedTotalValue.length === 0) {
+ return 0;
+ }
+ const number1 = Number(splitedTotalValue[0]);
+ const operator = splitedTotalValue[1].charAt(0);
+ ;
+ const number2 = Number(splitedTotalValue[2]);
+ switch (operator) {
+ case OPERATOR.ADD:
+ return number1 + number2;
+ case OPERATOR.SUBSTRACT:
+ return number1 - number2;
+ case OPERATOR.MULTIPLY:
+ return number1 * number2;
+ case OPERATOR.DIVIDE:
+ return Math.floor(number1 / number2);
+ default:
+ return 0;
+ }
+};
+export const makeFormula = (totalValue) => {
+ const totalValueArray = totalValue.match("(" + REGEXP.NUMBERS + ")(" + REGEXP.OPERATORS + ")(" + REGEXP.NUMBERS + ")");
+ if (totalValueArray) {
+ return [totalValueArray[1], totalValueArray[2], totalValueArray[3]];
+ }
+ return [];
+};
diff --git a/dist/ts/model/index.js b/dist/ts/model/index.js
new file mode 100644
index 0000000..525c3a5
--- /dev/null
+++ b/dist/ts/model/index.js
@@ -0,0 +1,18 @@
+"use strict";
+const calculate = (number1, number2, operator) => {
+ const operations = {
+ ["+"]: (x, y) => {
+ return x + y;
+ },
+ ["-"]: (x, y) => {
+ return x - y;
+ },
+ ["X"]: (x, y) => {
+ return x * y;
+ },
+ ["/"]: (x, y) => {
+ return Math.floor(x / y);
+ },
+ };
+ return operations[operator](number1, number2);
+};
diff --git a/dist/ts/model/validator.js b/dist/ts/model/validator.js
new file mode 100644
index 0000000..3918c74
--- /dev/null
+++ b/dist/ts/model/validator.js
@@ -0,0 +1 @@
+"use strict";
diff --git a/dist/ts/view/index.js b/dist/ts/view/index.js
new file mode 100644
index 0000000..89144e6
--- /dev/null
+++ b/dist/ts/view/index.js
@@ -0,0 +1,27 @@
+import { EQUAL } from "../constants/index.js";
+import { checkFirstTotalValue, checkPreventClickValue, isNumber, } from "../controller/validator.js";
+import { calculator, makeFormula } from "../model/calculator.js";
+export const clearTotalValue = () => {
+ document.getElementById("total").innerText = "0";
+};
+export const pushTotalValue = (e) => {
+ const clickTarget = e.target;
+ const totalTarget = document.getElementById("total");
+ const formula = makeFormula(totalTarget.innerText);
+ let clickValue = clickTarget.innerText;
+ if (totalTarget.innerText.length > 3 && isNumber(totalTarget.innerText)) {
+ return;
+ } // 계산 결과가 3자리를 넘어가는 경우 [AC] 외에 입력 방지
+ if (checkFirstTotalValue(totalTarget.innerText, clickValue)) {
+ totalTarget.innerText = clickValue;
+ return;
+ } // 첫번째 입력 시 0을 대체
+ if (checkPreventClickValue(totalTarget.innerText, clickValue)) {
+ return;
+ } // 입력 방지해야하는 상황 체크
+ if (clickValue === EQUAL) {
+ totalTarget.innerText = String(calculator(formula));
+ return;
+ } // [=]이 입력된다면 공식을 계산기에 넘겨서 값 받은 후 렌더링
+ totalTarget.innerText += clickValue;
+};
diff --git a/index.html b/index.html
index 8bf48ab..547d056 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
Calculator
-
+
@@ -34,5 +34,6 @@
0
+