diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..c5624c4e5
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,20 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 12,
+ "sourceType": "module"
+ },
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "rules": {
+ }
+}
diff --git a/.gitignore b/.gitignore
index 882ea6b5d..97b818cef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+package-lock.json
+yarn.lock
+
# Created by https://www.toptal.com/developers/gitignore/api/node,vscode,intellij,webstorm
# Edit at https://www.toptal.com/developers/gitignore?templates=node,vscode,intellij,webstorm
@@ -192,7 +195,7 @@ typings/
# Nuxt.js build / generate output
.nuxt
-dist
+# dist
# Gatsby files
.cache/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..dd75ac73f
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "parser": "typescript",
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 120
+ }
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1 @@
+{}
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..39c545fde
--- /dev/null
+++ b/cypress/integration/racingcar.spec.js
@@ -0,0 +1,139 @@
+import { ALERT, DELAY } from '../../dist/constants.js';
+
+let carNamesSample = 'jwon, yeji, holee, yshin';
+let tryCountSample = 3;
+
+const carNameInputAndSubmit = (carNames) => {
+ if (carNames) {
+ cy.get('#car-names-input').type(carNames);
+ }
+ cy.get('#car-names-submit').click();
+};
+
+const tryCountInputAndSubmit = (tryCount) => {
+ if (tryCount) {
+ cy.get('#racing-count-input').type(tryCount);
+ }
+ cy.get('#racing-count-submit').click();
+};
+
+const catchAlertMessage = (alertMessage) => {
+ cy.on('window:alert', txt => {
+ expect(txt).to.contains(alertMessage);
+ });
+};
+
+describe('0. 초기화면 로딩 테스트', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:5500');
+ });
+
+ it('자동차 경주 게임을 실행하면, 인풋 섹션이 보이고 자동차 입력창이 활성화된다.=', () => {
+ cy.get('#input-section').should('be.visible');
+ cy.get('#car-names-input').should('not.be.disabled');
+ cy.get('#car-names-submit').should('not.be.disabled');
+ });
+});
+
+describe('1. 자동차 이름 입력 테스트', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:5500');
+ });
+
+ it('자동차 이름을 입력하지 않고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit('');
+ catchAlertMessage(ALERT.CARNAME_NOTHING);
+ });
+
+ it('자동차 이름을 1개만 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit('jwon');
+ catchAlertMessage(ALERT.CARNAME_ALONE);
+ });
+
+ it('비어있는 자동차 이름을 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit('jwon, , holee');
+ catchAlertMessage(ALERT.CARNAME_EMPTY);
+ });
+
+ it('중복된 자동차 이름을 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit('jwon, jwon, yechoi, holee, yshin');
+ catchAlertMessage(ALERT.CARNAME_DOUBLE);
+ });
+
+ it('5글자 넘는 자동차 이름을 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit('jwon, yechoi');
+ catchAlertMessage(ALERT.CARNAME_LENGTH);
+ });
+
+ it('자동차 이름을 정상적으로 입력하고 확인 버튼을 클릭하면, 자동차 이름 입력칸과 클릭 버튼을 비활성화 하고 시도 횟수 입력칸과 시도 횟수 버튼을 활성화한다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ cy.get('#car-names-input').should('be.disabled');
+ cy.get('#car-names-submit').should('be.disabled');
+ cy.get('#racing-count-input').should('not.be.disabled');
+ cy.get('#racing-count-submit').should('not.be.disabled');
+ });
+
+ it('자동차 이름을 정상적으로 입력하고 확인 버튼을 클릭하면, 진행상황 섹션이 로딩된다. ', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(tryCountSample);
+ cy.get('#progress-section').should('be.visible');
+ });
+});
+
+describe('2. 시도 횟수 입력 테스트', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:5500');
+ });
+
+ it('횟수를 입력하지 않고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit();
+ catchAlertMessage(ALERT.TRYCOUNT_NOTHING);
+ });
+
+ it('음수를 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(-5);
+ catchAlertMessage(ALERT.TRYCOUNT_UINT);
+ });
+
+ it('50 이상을 입력하고 확인 버튼을 클릭하면, 경고창을 띄운다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(51);
+ catchAlertMessage(ALERT.TRYCOUNT_TOO_BIG);
+ });
+});
+
+describe('3. 진행 상황 출력 테스트', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:5500');
+ });
+
+ it('자동차 이름과 시도 횟수를 정상적으로 입력하고 확인 버튼을 클릭하면, 게임 진행 후 결과 섹션이 로딩된다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(tryCountSample);
+ });
+
+ it('게임이 끝나면, 결과를 출력하고 축하 메세지를 띄운다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(tryCountSample);
+ cy.wait(2000);
+ catchAlertMessage(ALERT.CONGRATULATION);
+ });
+});
+
+describe('4. 게임 재시작 테스트', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:5500');
+ });
+
+ it('다시 시작하기 버튼을 클릭하면, 진행 섹션과 결과 섹션이 사라지고, 자동차 이름 입력칸이 빈 상태로 게임 시작 대기상태가 된다', () => {
+ carNameInputAndSubmit(carNamesSample);
+ tryCountInputAndSubmit(tryCountSample);
+ cy.wait(tryCountSample * DELAY.GAME_TURN + DELAY.GAME_END);
+ cy.get('#restart-button').click();
+ cy.get('#progress-section').should('not.exist');
+ cy.get('#result-section').should('not.exist');
+ cy.get('#car-names-input').should('have.value', '');
+ });
+});
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/dist/constants.js b/dist/constants.js
new file mode 100644
index 000000000..7e06f9006
--- /dev/null
+++ b/dist/constants.js
@@ -0,0 +1,31 @@
+const DELAY = {
+ // millisecond(s)
+ GAME_TURN: 1000,
+ GAME_END: 2000,
+};
+const RULE = {
+ // random(0~9) 값이 4 이상일 경우 전진, 3 이하의 값이면 전진하지 않음
+ MIN_SCORE: 0,
+ MAX_SCORE: 9,
+ THRESHOULD_SCORE: 4,
+ // 자동차 이름은 5자 이하만 가능
+ MAX_CARNAME_LENGTH: 5,
+ // 시도 횟수는 원활한 게임을 위해 자체적으로 제한
+ MIN_TRYCOUNT: 1,
+ MAX_TRYCOUNT: 50,
+};
+const ALERT = {
+ // for check valid car name
+ CARNAME_NOTHING: '자동차 이름을 입력해주세요',
+ CARNAME_ALONE: '자동차 이름을 2개 이상 입력해주세요',
+ CARNAME_EMPTY: '비어 있는 자동차 이름이 있습니다',
+ CARNAME_DOUBLE: '중복된 자동차 이름이 있습니다',
+ CARNAME_LENGTH: '자동차 이름 글자수가 초과되었습니다 (5자 이내)',
+ // for check valid try count
+ TRYCOUNT_NOTHING: '시도 횟수를 입력해주세요',
+ TRYCOUNT_UINT: '시도 횟수는 양의 정수를 입력해주세요 (50 이하)',
+ TRYCOUNT_TOO_BIG: '시도 횟수가 너무 많습니다 (50 이하)',
+ // etc.
+ CONGRATULATION: '님 우승을 축하합니다 👏',
+};
+export { DELAY, RULE, ALERT };
diff --git a/dist/controller/gameController.js b/dist/controller/gameController.js
new file mode 100644
index 000000000..1e222bd10
--- /dev/null
+++ b/dist/controller/gameController.js
@@ -0,0 +1,36 @@
+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 { ALERT, DELAY } from '../constants.js';
+import { $ } from '../utils.js';
+import { Game } from '../model/Game.js';
+import { enableButton } from '../view/utils.js';
+import { renderArrowDiv, removeSpinnerDivs } from '../view/progressSectionRenderer.js';
+import { renderResultSection } from '../view/resultSectionRenderer.js';
+import { addRestartButtonEvent } from './restartController.js';
+const playOnce = (racingGame) => {
+ racingGame.play();
+ racingGame.roundWinners.forEach((index) => {
+ renderArrowDiv(index);
+ });
+ racingGame.initRoundWinners();
+};
+const startGame = (inputData) => __awaiter(void 0, void 0, void 0, function* () {
+ const racingGame = new Game(inputData.carNameArray);
+ for (let index = 0; index < inputData.tryCount; index += 1) {
+ yield racingGame.makeDelay(DELAY.GAME_TURN).then(() => playOnce(racingGame));
+ }
+ racingGame.judgeFinalWinners();
+ removeSpinnerDivs();
+ renderResultSection(racingGame.finalWinners.join(', ').toLowerCase());
+ yield racingGame.makeDelay(DELAY.GAME_END).then(() => alert(racingGame.finalWinners.join(', ') + ALERT.CONGRATULATION));
+ enableButton($('#restart-button'));
+ addRestartButtonEvent();
+});
+export { startGame };
diff --git a/dist/controller/inputController.js b/dist/controller/inputController.js
new file mode 100644
index 000000000..933b237e2
--- /dev/null
+++ b/dist/controller/inputController.js
@@ -0,0 +1,39 @@
+import { $ } from '../utils.js';
+import { startGame } from './gameController.js';
+import { InputData } from '../model/InputData.js';
+import { renderProgressSection } from '../view/progressSectionRenderer.js';
+import { clearInput, enableInput, disableInput, enableButton, disableButton } from '../view/utils.js';
+const getCarNames = (inputData) => {
+ const carNameInput = $('#car-names-input');
+ const carNameArray = carNameInput === null || carNameInput === void 0 ? void 0 : carNameInput.value.split(',').map((x) => x.trim());
+ if (inputData.checkCarNames(carNameInput, carNameArray)) {
+ renderProgressSection(carNameArray);
+ inputData.carNameArray = carNameArray;
+ disableInput(carNameInput);
+ disableButton($('#car-names-submit'));
+ enableInput($('#racing-count-input'));
+ enableButton($('#racing-count-submit'));
+ $('#racing-count-input').focus();
+ return;
+ }
+ clearInput(carNameInput);
+};
+const getTryCount = (inputData) => {
+ const tryCountInput = $('#racing-count-input');
+ const tryCount = Number(tryCountInput === null || tryCountInput === void 0 ? void 0 : tryCountInput.value);
+ if (inputData.checkTryCount(tryCountInput)) {
+ inputData.tryCount = tryCount;
+ disableInput(tryCountInput);
+ disableButton($('#racing-count-submit'));
+ startGame(inputData);
+ return;
+ }
+ clearInput(tryCountInput);
+};
+const addInputButtonsEvent = () => {
+ var _a, _b;
+ const inputData = new InputData();
+ (_a = $('#car-names-submit')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => getCarNames(inputData));
+ (_b = $('#racing-count-submit')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => getTryCount(inputData));
+};
+export { addInputButtonsEvent };
diff --git a/dist/controller/restartController.js b/dist/controller/restartController.js
new file mode 100644
index 000000000..fc03bb5e2
--- /dev/null
+++ b/dist/controller/restartController.js
@@ -0,0 +1,14 @@
+import { $ } from '../utils.js';
+import { removeChildNodes } from '../view/utils.js';
+import { renderInputSection } from '../view/inputSectionRenderer.js';
+const init = () => {
+ removeChildNodes($('#app'));
+ renderInputSection();
+ addRestartButtonEvent();
+ $('#car-names-input').focus();
+};
+const addRestartButtonEvent = () => {
+ var _a;
+ (_a = $('#restart-button')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => init());
+};
+export { addRestartButtonEvent };
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/index.js b/dist/index.js
new file mode 100644
index 000000000..c8bceb728
--- /dev/null
+++ b/dist/index.js
@@ -0,0 +1,7 @@
+import { renderInputSection } from './view/inputSectionRenderer.js';
+import { addInputButtonsEvent } from './controller/inputController.js';
+const app = () => {
+ renderInputSection();
+ addInputButtonsEvent();
+};
+app();
diff --git a/dist/model/Car.js b/dist/model/Car.js
new file mode 100644
index 000000000..ab30135a0
--- /dev/null
+++ b/dist/model/Car.js
@@ -0,0 +1,11 @@
+class Car {
+ constructor(name, index) {
+ this.name = name;
+ this.position = 0;
+ this.index = index;
+ }
+ moveForward() {
+ this.position += 1;
+ }
+}
+export { Car };
diff --git a/dist/model/Game.js b/dist/model/Game.js
new file mode 100644
index 000000000..cff8bc76c
--- /dev/null
+++ b/dist/model/Game.js
@@ -0,0 +1,59 @@
+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 { Car } from './Car.js';
+import { RULE } from '../constants.js';
+class Game {
+ constructor(carNameArray) {
+ this.cars = [];
+ this.finalWinners = [];
+ this.roundWinners = [];
+ this.maxPosition = 0;
+ this.setCarsObject(carNameArray);
+ }
+ play() {
+ this.cars.forEach((car) => {
+ if (Math.floor(Math.random() * RULE.MAX_SCORE) + RULE.MIN_SCORE >= RULE.THRESHOULD_SCORE) {
+ car.moveForward();
+ this.roundWinners.push(car.index);
+ }
+ });
+ this.updateMaxPosition();
+ }
+ setCarsObject(carNameArray) {
+ carNameArray.forEach((car, index) => {
+ this.cars.push(new Car(car, index));
+ });
+ }
+ ;
+ makeDelay(ms) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return new Promise((r) => setTimeout(r, ms));
+ });
+ }
+ ;
+ updateMaxPosition() {
+ this.cars.forEach((car) => {
+ if (car.position > this.maxPosition) {
+ this.maxPosition = car.position;
+ }
+ });
+ }
+ initRoundWinners() {
+ this.roundWinners = [];
+ }
+ judgeFinalWinners() {
+ this.cars.forEach((car) => {
+ if (car.position === this.maxPosition) {
+ this.finalWinners.push(car.name);
+ }
+ });
+ }
+}
+export { Game };
diff --git a/dist/model/InputData.js b/dist/model/InputData.js
new file mode 100644
index 000000000..0d0357cc7
--- /dev/null
+++ b/dist/model/InputData.js
@@ -0,0 +1,74 @@
+import { RULE, ALERT } from '../constants.js';
+class InputData {
+ constructor() {
+ this.carNameArray = [];
+ this.tryCount = 0;
+ }
+ checkEmptyInput(input) {
+ return input.value.trim() === '';
+ }
+ ;
+ checkArrayHasEmptyElement(array) {
+ return array.some((x) => x === '');
+ }
+ checkArrayHasOneElement(array) {
+ return array.length < 2;
+ }
+ ;
+ checkArrayDupElements(array) {
+ return array.some((x) => {
+ return array.indexOf(x) !== array.lastIndexOf(x);
+ });
+ }
+ ;
+ checkArrayElementsLength(array) {
+ for (let index = 0; index < array.length; index += 1) {
+ if (array[index].length > RULE.MAX_CARNAME_LENGTH) {
+ return true;
+ }
+ }
+ return false;
+ }
+ ;
+ checkCarNames(carNameInput, carNameArray) {
+ if (this.checkEmptyInput(carNameInput)) {
+ alert(ALERT.CARNAME_NOTHING);
+ return false;
+ }
+ else if (this.checkArrayHasOneElement(carNameArray)) {
+ alert(ALERT.CARNAME_ALONE);
+ return false;
+ }
+ else if (this.checkArrayHasEmptyElement(carNameArray)) {
+ alert(ALERT.CARNAME_EMPTY);
+ return false;
+ }
+ else if (this.checkArrayDupElements(carNameArray)) {
+ alert(ALERT.CARNAME_DOUBLE);
+ return false;
+ }
+ else if (this.checkArrayElementsLength(carNameArray)) {
+ alert(ALERT.CARNAME_LENGTH);
+ return false;
+ }
+ return true;
+ }
+ ;
+ checkTryCount(tryCountInput) {
+ if (this.checkEmptyInput(tryCountInput)) {
+ alert(ALERT.TRYCOUNT_NOTHING);
+ return false;
+ }
+ else if (Number(tryCountInput.value) < RULE.MIN_TRYCOUNT) {
+ alert(ALERT.TRYCOUNT_UINT);
+ return false;
+ }
+ else if (Number(tryCountInput.value) > RULE.MAX_TRYCOUNT) {
+ alert(ALERT.TRYCOUNT_TOO_BIG);
+ return false;
+ }
+ return true;
+ }
+ ;
+}
+export { InputData };
diff --git a/dist/utils.js b/dist/utils.js
new file mode 100644
index 000000000..bfea08cd6
--- /dev/null
+++ b/dist/utils.js
@@ -0,0 +1,3 @@
+const $ = (selector) => document.querySelector(selector);
+const $$ = (selector) => document.querySelectorAll(selector);
+export { $, $$ };
diff --git a/dist/view/inputSectionRenderer.js b/dist/view/inputSectionRenderer.js
new file mode 100644
index 000000000..08c7b1cde
--- /dev/null
+++ b/dist/view/inputSectionRenderer.js
@@ -0,0 +1,31 @@
+import { $ } from '../utils.js';
+const inputSection = () => {
+ return `
+
+ `;
+};
+const renderInputSection = () => {
+ $('#app').innerHTML = inputSection();
+};
+export { renderInputSection };
diff --git a/dist/view/progressSectionRenderer.js b/dist/view/progressSectionRenderer.js
new file mode 100644
index 000000000..f2b0a01f4
--- /dev/null
+++ b/dist/view/progressSectionRenderer.js
@@ -0,0 +1,50 @@
+import { $, $$ } from '../utils.js';
+const progressSection = () => {
+ return `
+
+ `;
+};
+const carNameDiv = (carName) => {
+ return `
+
+ `;
+};
+const spinnerDiv = () => {
+ return `
+
+ `;
+};
+const arrowDiv = () => {
+ return `
+ ⬇️️
+ `;
+};
+const renderProgressSection = (carNameArray) => {
+ $('#app').insertAdjacentHTML('beforeend', progressSection());
+ carNameArray.forEach((carName) => {
+ $('#progress-section .mt-4').insertAdjacentHTML('beforeend', carNameDiv(carName));
+ });
+ $$('#progress-section .mr-2').forEach((element) => {
+ element.insertAdjacentHTML(`beforeend`, spinnerDiv());
+ });
+};
+const renderArrowDiv = (roundWinnerIndex) => {
+ $$('#progress-section .car-player').forEach((element, index) => {
+ index === roundWinnerIndex ? element.insertAdjacentHTML(`afterend`, arrowDiv()) : null;
+ });
+};
+const removeSpinnerDivs = () => {
+ $$('#progress-section .mt-3').forEach((element) => {
+ element.remove();
+ });
+};
+export { renderProgressSection, renderArrowDiv, removeSpinnerDivs };
diff --git a/dist/view/resultSectionRenderer.js b/dist/view/resultSectionRenderer.js
new file mode 100644
index 000000000..494468012
--- /dev/null
+++ b/dist/view/resultSectionRenderer.js
@@ -0,0 +1,17 @@
+import { $ } from '../utils.js';
+const resultSection = (winners) => {
+ return `
+
+
+
🏆 최종 우승자: ${winners} 🏆
+
+
+
+
+
+ `;
+};
+const renderResultSection = (winners) => {
+ $('#app').insertAdjacentHTML('beforeend', resultSection(winners));
+};
+export { renderResultSection };
diff --git a/dist/view/utils.js b/dist/view/utils.js
new file mode 100644
index 000000000..4567fa02d
--- /dev/null
+++ b/dist/view/utils.js
@@ -0,0 +1,27 @@
+const clearInput = (element) => {
+ element.value = '';
+ element.focus();
+};
+const enableInput = (element) => {
+ element.disabled = false;
+};
+const disableInput = (element) => {
+ element.disabled = true;
+};
+const enableButton = (element) => {
+ element.disabled = false;
+};
+const disableButton = (element) => {
+ element.disabled = true;
+};
+const removeChildNodes = (element) => {
+ if (!element) {
+ return;
+ }
+ while (element.hasChildNodes()) {
+ if (element.lastChild) {
+ element.removeChild(element.lastChild);
+ }
+ }
+};
+export { clearInput, enableInput, disableInput, enableButton, disableButton, removeChildNodes };
diff --git a/index.html b/index.html
index ea5891942..846cbd5b4 100644
--- a/index.html
+++ b/index.html
@@ -3,69 +3,11 @@
🏎️ 자동차 경주 게임
-
+
-
-
-
-
-
🏆 최종 우승자: EAST, WEST 🏆
-
-
-
-
-
+