diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..f350d16f0
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,15 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": ["airbnb-base"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+ "plugins": ["@typescript-eslint"],
+ "ignorePatterns": ["dist/", "node_modules/"],
+ "rules": {}
+}
diff --git a/.gitignore b/.gitignore
index 882ea6b5d..ded4e8434 100644
--- a/.gitignore
+++ b/.gitignore
@@ -192,7 +192,6 @@ typings/
# Nuxt.js build / generate output
.nuxt
-dist
# Gatsby files
.cache/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..9db28a2bd
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 120
+}
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 000000000..f348ac0ce
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,3 @@
+{
+ "baseUrl": "http://127.0.0.1:5500"
+}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 000000000..02e425437
--- /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/integration/racingcar.spec.js b/cypress/integration/racingcar.spec.js
new file mode 100644
index 000000000..2c85353f2
--- /dev/null
+++ b/cypress/integration/racingcar.spec.js
@@ -0,0 +1,134 @@
+const typeAndCheckInput = (location, content, result) => {
+ cy.get(location).type(content);
+ cy.get(location).should('have.value', result);
+};
+
+const typeAndClickCarNameSubmit = (content) => {
+ typeAndCheckInput('.w-100[type=text]', content, content);
+ cy.get('#car-name-submit').click();
+ cy.get('#car-name-submit').should('be.disabled');
+ cy.get('.w-100[type=text]').should('be.disabled');
+};
+
+const typeAndClickCountSubmit = (count) => {
+ typeAndCheckInput('.w-100[type=number]', count, count);
+ cy.get('#car-count-submit').click();
+ cy.get('#car-count-submit').should('be.disabled');
+ cy.get('.w-100[type=number]').should('be.disabled');
+};
+
+const checkSpinnerCount = (length) => {
+ cy.get('.spinner-container').should('exist');
+ cy.get('.mr-2').find('.spinner').should('have.length', length);
+};
+
+describe('initial page behavior', () => {
+ beforeEach(() => {
+ cy.visit('/javascript-racingcar/');
+ });
+ it('input값 입력 했을때 잘 들어가는지 확인', () => {
+ cy.get('.w-100[type=text]').type('holee');
+ cy.get('.w-100[type=text]').should('have.value', 'holee');
+ cy.get('.w-100[type=number]').type(1);
+ cy.get('.w-100[type=number]').should('have.value', 1);
+ });
+});
+
+describe('racing-game base behavior', () => {
+ beforeEach(() => {
+ cy.visit('/javascript-racingcar/');
+ });
+ const basicInput = 'EAST, WEST, SOUTH, NORTH';
+ const basicCount = 2;
+
+ it('자동차 이름 입력 했을 때, car name input/button 비활성화', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ });
+ it('자동차 이름 입력 했을 때, 이름에 맞는 자동차/스피너 생성', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ basicInput.split(', ').map((x, i) => {
+ cy.get('.car-player').eq(i).should('have.text', x);
+ });
+ checkSpinnerCount(basicInput.split(', ').length);
+ });
+ it('시도 횟수 입력 했을 때, count input/button 비활성화', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ typeAndClickCountSubmit(basicCount);
+ });
+ it('시도 횟수 입력 했을 때, 알맞은 ⬇️️ 생성 및 spinner 삭제', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ typeAndClickCountSubmit(basicCount);
+ cy.clock();
+ checkSpinnerCount(basicInput.split(', ').length);
+ cy.tick(2000);
+ cy.get('.forward-icon').should('exist');
+ checkSpinnerCount(0);
+ });
+ it('시도 횟수 입력 했을 때, 결과 화면 출력', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ typeAndClickCountSubmit(basicCount);
+ cy.wait(2000);
+ cy.get('section').eq(2).should('exist');
+ });
+ it('시도 횟수 입력 했을 때, 알맞은 alert 생성', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ typeAndClickCountSubmit(basicCount);
+ cy.wait(2000);
+ cy.get('section').eq(2).should('exist');
+ cy.wait(1500);
+ cy.on('window:alert', (txt) => {
+ expect(txt).to.contains('🏆 우승 축하합니다 ㅎㅎ 🏆');
+ });
+ });
+ it('다시 시작하기 버튼 불렀을 때, html 초기화 확인', () => {
+ typeAndClickCarNameSubmit(basicInput);
+ typeAndClickCountSubmit(basicCount);
+ cy.wait(3500);
+ cy.get('button').eq(2).click();
+ cy.get('section').eq(1).should('not.exist');
+ cy.get('section').eq(2).should('not.exist');
+ cy.get('#car-name-submit').should('not.disabled');
+ cy.get('.w-100[type=text]').should('not.disabled');
+ cy.get('#car-count-submit').should('not.disabled');
+ cy.get('.w-100[type=number]').should('not.disabled');
+ });
+});
+
+const checkValidInit = () => {
+ cy.get('#car-count-submit').should('not.disabled');
+ cy.get('.w-100[type=number]').should('not.disabled');
+ cy.get('section').eq(1).should('not.exist');
+ cy.get('section').eq(2).should('not.exist');
+ cy.get('.w-100[type=text]').focus();
+};
+
+describe('racing-game exception behavior', () => {
+ beforeEach(() => {
+ cy.visit('/javascript-racingcar/');
+ });
+ const basicInput = 'EAST, WEST, SOUTH, NORTH';
+ const basicCount = 2;
+
+ it('자동차 이름을 입력하지 않았을 때, 시도할 횟수 버튼 클릭', () => {
+ cy.get('#car-name-submit').click();
+ cy.get('#car-count-submit').click();
+ checkValidInit();
+ });
+ it('자동차 이름을 입력하지 않았을 때, 시도할 횟수 입력 후 버튼 클릭', () => {
+ typeAndCheckInput('.w-100[type=number]', basicCount, basicCount);
+ cy.get('#car-name-submit').click();
+ cy.get('#car-count-submit').click();
+ checkValidInit();
+ });
+ it('자동차 이름을 입력한 후 확인 버튼을 누르지않았을 때, 시도할 횟수 버튼 클릭', () => {
+ typeAndCheckInput('.w-100[type=text]', basicInput, basicInput);
+ cy.get('#car-count-submit').click();
+ checkValidInit();
+ });
+ it('자동차 이름을 입력한 후 확인 버튼을 누르지않았을 때, 시도할 횟수 입력 후 버튼 클릭', () => {
+ typeAndCheckInput('.w-100[type=text]', basicInput, basicInput);
+ typeAndCheckInput('.w-100[type=number]', basicCount, basicCount);
+ cy.get('#car-count-submit').click();
+ checkValidInit();
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 000000000..59b2bab6e
--- /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 000000000..119ab03f7
--- /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 000000000..d68db96df
--- /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/src/css/index.css b/dist/css/index.css
similarity index 100%
rename from src/css/index.css
rename to dist/css/index.css
diff --git a/src/css/shared/button.css b/dist/css/shared/button.css
similarity index 100%
rename from src/css/shared/button.css
rename to dist/css/shared/button.css
diff --git a/src/css/shared/layout.css b/dist/css/shared/layout.css
similarity index 100%
rename from src/css/shared/layout.css
rename to dist/css/shared/layout.css
diff --git a/src/css/shared/sizing.css b/dist/css/shared/sizing.css
similarity index 100%
rename from src/css/shared/sizing.css
rename to dist/css/shared/sizing.css
diff --git a/src/css/shared/typhography.css b/dist/css/shared/typhography.css
similarity index 100%
rename from src/css/shared/typhography.css
rename to dist/css/shared/typhography.css
diff --git a/src/css/ui/spinner.css b/dist/css/ui/spinner.css
similarity index 100%
rename from src/css/ui/spinner.css
rename to dist/css/ui/spinner.css
diff --git a/dist/js/index.js b/dist/js/index.js
new file mode 100644
index 000000000..f62346eb7
--- /dev/null
+++ b/dist/js/index.js
@@ -0,0 +1,7 @@
+import Game from './modules/Game.js';
+export default function App() {
+ Game({
+ $app: document.querySelector('#app'),
+ });
+}
+App();
diff --git a/dist/js/modules/@share/constants.js b/dist/js/modules/@share/constants.js
new file mode 100644
index 000000000..501dc12cb
--- /dev/null
+++ b/dist/js/modules/@share/constants.js
@@ -0,0 +1,13 @@
+const ERROR_MESSAGE = {
+ INVALID_CAR_NAME_INPUT: '유효하지 않은 자동차 이름입니다. 다시 입력 해주세요.',
+ INVALID_INPUT_PROCEDURE: '자동차 이름 먼저 입력해주세요!',
+ INVALID_COUNT_INPUT: '유효하지 않은 횟수 입력입니다. 다시 입력 해주세요.',
+};
+const MESSAGE = {
+ CELEBRATE_WINNER: '🏆 우승 축하합니다 ㅎㅎ 🏆',
+};
+const DELAY = {
+ RACE: 1000,
+ ALERT: 1500,
+};
+export { ERROR_MESSAGE, MESSAGE, DELAY };
diff --git a/dist/js/modules/@share/controller.js b/dist/js/modules/@share/controller.js
new file mode 100644
index 000000000..4ad0ad09d
--- /dev/null
+++ b/dist/js/modules/@share/controller.js
@@ -0,0 +1,38 @@
+import CarNameComponent from '../CarName.js';
+import RaceComponent from '../Race.js';
+import { $, $$, disable, initEnable } from './utils.js';
+import { ERROR_MESSAGE } from './constants.js';
+import { carNameInputInit } from './init.js';
+const carNameInputEvent = () => {
+ var _a;
+ const carNamesInput = $('#car-name-input');
+ disable(carNamesInput);
+ disable((_a = carNamesInput.parentElement) === null || _a === void 0 ? void 0 : _a.children[1]);
+ if (carNamesInput) {
+ CarNameComponent({ $app: $('#app'), carNames: carNamesInput.value });
+ }
+};
+const raceCountInputEvent = () => {
+ var _a;
+ const raceCountInput = $('input[type="number"]');
+ const carNamesInput = $('input[type="text"]');
+ disable(raceCountInput);
+ disable((_a = raceCountInput.parentElement) === null || _a === void 0 ? void 0 : _a.children[1]);
+ if (raceCountInput && carNamesInput.value !== '') {
+ RaceComponent({
+ $app: $('#app'),
+ count: Number(raceCountInput.value),
+ });
+ }
+ else {
+ alert(ERROR_MESSAGE.INVALID_INPUT_PROCEDURE);
+ carNameInputInit();
+ initEnable();
+ }
+};
+const inputController = () => {
+ const gameButton = $$('button');
+ gameButton[0].onclick = carNameInputEvent;
+ gameButton[1].onclick = raceCountInputEvent;
+};
+export { inputController };
diff --git a/dist/js/modules/@share/dom-dataset.js b/dist/js/modules/@share/dom-dataset.js
new file mode 100644
index 000000000..3c4df00fb
--- /dev/null
+++ b/dist/js/modules/@share/dom-dataset.js
@@ -0,0 +1,15 @@
+import { $ } from './utils.js';
+const setCarNameDataset = (data) => {
+ const carNamesInput = $('input[type="text"]');
+ if (carNamesInput) {
+ carNamesInput.dataset.click = data;
+ }
+};
+const checkCarNameDataset = () => {
+ const carNamesInput = $('input[type="text"]');
+ if (carNamesInput) {
+ return carNamesInput.dataset.click === 'click';
+ }
+ return false;
+};
+export { setCarNameDataset, checkCarNameDataset };
diff --git a/dist/js/modules/@share/init.js b/dist/js/modules/@share/init.js
new file mode 100644
index 000000000..1ccdac612
--- /dev/null
+++ b/dist/js/modules/@share/init.js
@@ -0,0 +1,12 @@
+import { $ } from './utils.js';
+const carNameInputInit = () => {
+ const carNameInput = $('#car-name-input');
+ carNameInput.value = '';
+ carNameInput.focus();
+};
+const racingCountInputInit = () => {
+ const racingCountInput = $('#racing-count-input');
+ racingCountInput.value = '';
+ racingCountInput.focus();
+};
+export { carNameInputInit, racingCountInputInit };
diff --git a/dist/js/modules/@share/message.js b/dist/js/modules/@share/message.js
new file mode 100644
index 000000000..86e699fa7
--- /dev/null
+++ b/dist/js/modules/@share/message.js
@@ -0,0 +1,8 @@
+export const ERROR_MESSAGE = {
+ INVALID_CAR_NAME_INPUT: '유효하지 않은 자동차 이름입니다. 다시 입력 해주세요.',
+ INVALID_INPUT_PROCEDURE: '자동차 이름 먼저 입력해주세요!',
+ INVALID_COUNT_INPUT: '유효하지 않은 횟수 입력입니다. 다시 입력 해주세요.'
+};
+export const MESSAGE = {
+ CELEBRATE_WINNER: '🏆 축하합니다 ㅎㅎ 최종 우승자: EAST, WEST, SOUTH, NORTH 🏆'
+};
diff --git a/dist/js/modules/@share/spinner.js b/dist/js/modules/@share/spinner.js
new file mode 100644
index 000000000..0b42d2409
--- /dev/null
+++ b/dist/js/modules/@share/spinner.js
@@ -0,0 +1,7 @@
+const removeSpinner = (carPlayer) => {
+ var _a, _b, _c, _d;
+ if (((_b = (_a = carPlayer.parentNode) === null || _a === void 0 ? void 0 : _a.lastElementChild) === null || _b === void 0 ? void 0 : _b.className) === 'd-flex justify-center mt-3') {
+ (_d = (_c = carPlayer.parentNode) === null || _c === void 0 ? void 0 : _c.lastElementChild) === null || _d === void 0 ? void 0 : _d.remove();
+ }
+};
+export { removeSpinner };
diff --git a/dist/js/modules/@share/utils.js b/dist/js/modules/@share/utils.js
new file mode 100644
index 000000000..b8f4c7e90
--- /dev/null
+++ b/dist/js/modules/@share/utils.js
@@ -0,0 +1,54 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+class Car {
+ constructor(name) {
+ this.name = name;
+ this.distance = 0;
+ }
+ move() {
+ this.distance += 1;
+ }
+}
+const getRandomSingleDigit = (min, max) => {
+ return Math.floor(Math.random() * (max + 1 - min)) + min;
+};
+const wait = (delay) => __awaiter(void 0, void 0, void 0, function* () {
+ return new Promise((resolve) => setTimeout(resolve, delay));
+});
+const $ = (selector) => document.querySelector(selector);
+const $$ = (selector) => document.querySelectorAll(selector);
+const disable = (element) => {
+ element.disabled = true;
+};
+const enable = (element) => {
+ element.disabled = false;
+};
+const carNameEnable = () => {
+ var _a;
+ const carNamesInput = $('#car-name-input');
+ enable(carNamesInput);
+ enable((_a = carNamesInput.parentElement) === null || _a === void 0 ? void 0 : _a.children[1]);
+};
+const countEnable = () => {
+ var _a;
+ const raceCountInput = $('input[type="number"]');
+ enable(raceCountInput);
+ enable((_a = raceCountInput.parentElement) === null || _a === void 0 ? void 0 : _a.children[1]);
+};
+const initEnable = () => {
+ var _a, _b;
+ const carNamesInput = $('#car-name-input');
+ const raceCountInput = $('input[type="number"]');
+ enable(carNamesInput);
+ enable((_a = carNamesInput.parentElement) === null || _a === void 0 ? void 0 : _a.children[1]);
+ enable(raceCountInput);
+ enable((_b = raceCountInput.parentElement) === null || _b === void 0 ? void 0 : _b.children[1]);
+};
+export { Car, getRandomSingleDigit, wait, $, $$, disable, initEnable, carNameEnable, countEnable };
diff --git a/dist/js/modules/@share/view.js b/dist/js/modules/@share/view.js
new file mode 100644
index 000000000..2706647c2
--- /dev/null
+++ b/dist/js/modules/@share/view.js
@@ -0,0 +1,61 @@
+const startSection = ``;
+const carNamesSection = (carNames) => {
+ return ``;
+};
+const carNameDiv = (carName) => {
+ return `
`;
+};
+const winnerSection = (winner) => {
+ return `
+
+
🏆 최종 우승자: ${winner} 🏆
+
+
+
+
+ `;
+};
+const forwardIconDiv = `⬇️️
`;
+export { startSection, carNamesSection, carNameDiv, winnerSection, forwardIconDiv };
diff --git a/dist/js/modules/CarName.js b/dist/js/modules/CarName.js
new file mode 100644
index 000000000..f187a9355
--- /dev/null
+++ b/dist/js/modules/CarName.js
@@ -0,0 +1,29 @@
+import { carNamesSection, carNameDiv } from './@share/view.js';
+import { setCarNameDataset } from './@share/dom-dataset.js';
+import { ERROR_MESSAGE } from './@share/constants.js';
+import { carNameInputInit } from './@share/init.js';
+import { initEnable } from './@share/utils.js';
+const CarNameComponent = ({ $app, carNames }) => {
+ const checkCarNames = (carNameList) => carNameList.length === carNameList.filter((x) => x.length <= 5 && x !== '').length;
+ const render = (JSX) => {
+ const sectionElement = $app;
+ if (sectionElement) {
+ sectionElement.insertAdjacentHTML('beforeend', JSX);
+ }
+ return;
+ };
+ const init = (carNames) => {
+ const carNameList = carNames.split(',').map((x) => x.trim());
+ if (checkCarNames(carNameList)) {
+ setCarNameDataset('click');
+ render(carNamesSection(carNameList.map((carName) => carNameDiv(carName)).join('')));
+ }
+ else {
+ alert(ERROR_MESSAGE.INVALID_CAR_NAME_INPUT);
+ carNameInputInit();
+ initEnable();
+ }
+ };
+ init(carNames);
+};
+export default CarNameComponent;
diff --git a/dist/js/modules/CarNameComponent.js b/dist/js/modules/CarNameComponent.js
new file mode 100644
index 000000000..de0ce2137
--- /dev/null
+++ b/dist/js/modules/CarNameComponent.js
@@ -0,0 +1,34 @@
+const carNamesSection = (carNames) => {
+ return ``;
+};
+const carNameDiv = (carName) => {
+ return ``;
+};
+const CarNameComponent = ({ $app, carNames }) => {
+ const checkCarNames = (carNameList) => carNameList.length === carNameList.filter(x => x.length <= 5 && x !== '').length;
+ const render = (JSX) => {
+ const sectionElement = $app;
+ if (sectionElement) {
+ sectionElement.insertAdjacentHTML('beforeend', JSX);
+ }
+ return;
+ };
+ const init = (carNames) => {
+ const carNameList = carNames.split(',').map(x => x.trim());
+ if (checkCarNames(carNameList)) {
+ render(carNamesSection(carNameList.map(carName => carNameDiv(carName)).join('')));
+ }
+ };
+ init(carNames);
+};
+export default CarNameComponent;
+// ⬇️️
+//
+//
+//
diff --git a/dist/js/modules/Game.js b/dist/js/modules/Game.js
new file mode 100644
index 000000000..34c50384b
--- /dev/null
+++ b/dist/js/modules/Game.js
@@ -0,0 +1,17 @@
+import { inputController } from './@share/controller.js';
+import { startSection } from './@share/view.js';
+const Game = ({ $app }) => {
+ const render = (JSX) => {
+ const sectionElement = $app;
+ if (sectionElement) {
+ sectionElement.insertAdjacentHTML('beforeend', JSX);
+ }
+ return;
+ };
+ const init = () => {
+ render(startSection);
+ inputController();
+ };
+ init();
+};
+export default Game;
diff --git a/dist/js/modules/Race.js b/dist/js/modules/Race.js
new file mode 100644
index 000000000..058848a1d
--- /dev/null
+++ b/dist/js/modules/Race.js
@@ -0,0 +1,73 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+import WinnerComponent from './Winner.js';
+import { $, $$, Car, getRandomSingleDigit, wait, initEnable, countEnable } from './@share/utils.js';
+import { racingCountInputInit, carNameInputInit } from './@share/init.js';
+import { ERROR_MESSAGE, MESSAGE, DELAY } from './@share/constants.js';
+import { checkCarNameDataset } from './@share/dom-dataset.js';
+import { removeSpinner } from './@share/spinner.js';
+import { forwardIconDiv } from './@share/view.js';
+const RaceComponent = ({ $app, count }) => {
+ let _cars;
+ const checkValidCount = (count) => {
+ return count - Math.floor(count) === 0 && count > 0;
+ };
+ const getInputCarsName = () => {
+ const carNameInput = $('#car-name-input');
+ return carNameInput.value.split(',').map((x) => x.trim());
+ };
+ const assignCarsName = () => {
+ const cars = [];
+ const inputCarNames = getInputCarsName();
+ inputCarNames.forEach((name) => {
+ cars.push(new Car(name));
+ });
+ return cars;
+ };
+ const render = ({ count }) => __awaiter(void 0, void 0, void 0, function* () {
+ const carPlayer = $$('.car-player');
+ let tryCount = count;
+ while (tryCount > 0) {
+ for (let i = 0; i < carPlayer.length; i += 1) {
+ if (getRandomSingleDigit(0, 9) >= 4) {
+ _cars[i].move();
+ carPlayer[i].insertAdjacentHTML('afterend', forwardIconDiv);
+ }
+ if (tryCount <= 1) {
+ removeSpinner(carPlayer[i]);
+ }
+ }
+ tryCount -= 1;
+ yield wait(DELAY.RACE);
+ }
+ });
+ const init = (count) => __awaiter(void 0, void 0, void 0, function* () {
+ if (!checkCarNameDataset()) {
+ alert(ERROR_MESSAGE.INVALID_INPUT_PROCEDURE);
+ carNameInputInit();
+ initEnable();
+ return;
+ }
+ if (!checkValidCount(count)) {
+ alert(ERROR_MESSAGE.INVALID_COUNT_INPUT);
+ racingCountInputInit();
+ countEnable();
+ return;
+ }
+ _cars = assignCarsName();
+ yield render({ count });
+ WinnerComponent({ $app, cars: _cars });
+ yield wait(DELAY.ALERT);
+ alert(MESSAGE.CELEBRATE_WINNER);
+ return;
+ });
+ init(count);
+};
+export default RaceComponent;
diff --git a/dist/js/modules/Winner.js b/dist/js/modules/Winner.js
new file mode 100644
index 000000000..1c69d3173
--- /dev/null
+++ b/dist/js/modules/Winner.js
@@ -0,0 +1,43 @@
+import { setCarNameDataset } from './@share/dom-dataset.js';
+import { inputController } from './@share/controller.js';
+import { $$, initEnable } from './@share/utils.js';
+import { winnerSection } from './@share/view.js';
+const WinnerComponent = ({ $app, cars }) => {
+ const findWinners = (cars) => {
+ const totalDistances = cars.map((car) => car.distance);
+ const maxDistance = Math.max(...totalDistances);
+ return cars.filter((car) => car.distance === maxDistance).map((winner) => winner.name);
+ };
+ const render = (JSX) => {
+ const sectionElement = $app;
+ if (sectionElement) {
+ sectionElement.insertAdjacentHTML('beforeend', JSX);
+ }
+ return;
+ };
+ const retryButtonEvent = (e) => {
+ var _a;
+ if ($app) {
+ $app.children[2].remove();
+ $app.children[1].remove();
+ $app.innerHTML = $app.children[0].outerHTML;
+ }
+ (_a = e.currentTarget) === null || _a === void 0 ? void 0 : _a.removeEventListener('click', retryButtonEvent);
+ inputController();
+ initEnable();
+ setCarNameDataset('');
+ return;
+ };
+ const controller = () => {
+ const retryButton = $$('button')[2];
+ if (retryButton) {
+ retryButton.onclick = retryButtonEvent;
+ }
+ };
+ const init = ({ cars }) => {
+ render(winnerSection(findWinners(cars).join(', ')));
+ controller();
+ };
+ return init({ cars });
+};
+export default WinnerComponent;
diff --git a/dist/js/modules/utils.js b/dist/js/modules/utils.js
new file mode 100644
index 000000000..0b5d64474
--- /dev/null
+++ b/dist/js/modules/utils.js
@@ -0,0 +1,25 @@
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+class Car {
+ constructor(name) {
+ this.name = name;
+ this.distance = 0;
+ }
+ move() {
+ this.distance += 1;
+ }
+}
+const getRandomSingleDigit = (min, max) => {
+ return Math.floor(Math.random() * (max + 1 - min)) + min;
+};
+const wait = (delay) => __awaiter(void 0, void 0, void 0, function* () {
+ return new Promise((resolve) => setTimeout(resolve, delay));
+});
+export { Car, getRandomSingleDigit, wait };
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..c065c0d2d
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,35 @@
+## 기능 구현
+
+- [x] 자동차 이름 입력시 화면에 이름 렌더링
+ - [x] checkInput
+ - [x] render car names input
+- [x] 중간 경주 로직 만들기
+- [x] 시도할 횟수 '확인' 버튼 누를 때 경주 시작
+ - [x] 다시 시작하기 버튼 클릭할 때 초기화.
+ - [x] 자동차 이름 먼저 입력하게 하기
+
+### 🎯step 1
+
+- [x] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
+- [x] 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
+- [x] 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
+- [x] 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
+- [x] 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
+- [x] 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
+- [x] 우승자가 여러명일 경우 ,를 이용하여 구분한다.
+
+### 🎯🎯 step 2
+
+- [x] spinner
+ - [x] car-name 버튼 누를 때 spinner 렌더링 하기
+ - [x] 해당 자동차 경주 시작할 때 삭제하기
+- [x] 자동차 경주 게임의 턴이 진행 될 때마다 1초의 텀(progressive 재생)을 두고 진행한다.
+ - [x] 애니메이션 구현을 위해 setInterval, setTimeout, requestAnimationFrame 을 활용한다.
+ - [x] spinner 마지막 레이스가 완료 됐을 때 없애기.
+- [x] 정상적으로 게임의 턴이 다 동작된 후에는 결과를 보여주고, 2초 후에 축하의 alert 메세지를 띄운다.
+- [x] 위 기능들이 정상적으로 동작하는지 Cypress를 이용해 테스트한다.
+
+### 버그 수정
+- [x] car name input 입력한 뒤에만 count 입력할 수 있게 구현하기.
+- [x] input 입력한 뒤 비활성화 하기.
+ - [x] 각 에러 경우 후 활성화 하기.
diff --git a/index.html b/index.html
index ea5891942..4cc03c5ca 100644
--- a/index.html
+++ b/index.html
@@ -3,69 +3,10 @@
🏎️ 자동차 경주 게임
-
+
-
-
-
-
-
-
🏆 최종 우승자: EAST, WEST 🏆
-
-
-
-
-
-
+
+