Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2단계 - 자동차 경주 구현] 지그(송지은) 미션 제출합니다. #36

Merged
merged 7 commits into from
Feb 16, 2021
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<img width="400" src="https://techcourse-storage.s3.ap-northeast-2.amazonaws.com/7c76e809d82a4a3aa0fd78a86be25427">
</p>

### ✅ Todos
### ✅ Todos - 1단계
- [x] eslint, prettier 세팅
- [x] cypress 세팅
- [x] 자동차 이름 입력 받아서 배열에 저장한다.
Expand All @@ -38,6 +38,10 @@
- [x] 리팩토링: 테스트 코드 함수화
- [x] 리팩토링: 모듈 분리 & 상수화

### ✅ Todos - 2단계
- [x] 자동차 경주 게임의 턴이 진행 될 때마다 1초의 텀(progressive 재생)을 두고 진행한다.
- [x] 정상적으로 게임의 턴이 다 동작된 후에는 결과를 보여주고, 2초 후에 축하의 alert 메세지를 띄운다.

### 🎯 step1
- [ ] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- [ ] 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
Expand Down
43 changes: 39 additions & 4 deletions cypress/integration/racing.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { DELAY } from '../../src/js/constants.js';

describe('자동차 경주', () => {
const DEFAULT_TRY_COUNT = 5
const DEFAULT_CAR_UNITS = 4
const DEFAULT_TRY_COUNT_TIME = DEFAULT_TRY_COUNT * DELAY.TURN_TIME;

beforeEach(() => {
cy.visit('http://localhost:5501');
});
Expand All @@ -10,7 +16,7 @@ describe('자동차 경주', () => {
cy.get('.car-name-btn').click();
}

function clickAfterTypeTryCount(tryCount = 5) {
function clickAfterTypeTryCount(tryCount = DEFAULT_TRY_COUNT) {
if (tryCount) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파라미터에 디폴트 값을 받고 있으니 tryCount가 존재하는지 확인하는 if문은 없어도 될 것 같아요!

cy.get('.try-count').type(tryCount);
}
Expand Down Expand Up @@ -88,10 +94,24 @@ describe('자동차 경주', () => {
exceptionAlert('.try-count', '정수를 입력해주세요.');
});

it('레이싱 진행 상황과 함께 우승자가 출력된다', () => {
it('자동차 이름과 시도 횟수 입력 후 확인 버튼을 누르면 1초마다 spinner와 결과가 출력된다', () => {
clickAfterTypeCar();
clickAfterTypeTryCount();

for (let i = 0; i < DEFAULT_TRY_COUNT; i++) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let i for 문은 지양하는 게 좋습니다.
[...Array(DEFAULT_TRY_COUNT)].map(() => ...)으로 바꿔보는 건 어떨까요?

cy.get('.spinner-container').should('have.length', DEFAULT_CAR_UNITS);
cy.wait(DELAY.SPINNER_SEC);
cy.get('.spinner-container').should('not.be.visible');
cy.get('.forward-icon').should('be.visible');
cy.wait(DELAY.ARROW_DISPLAYING_SEC);
}
})

it('진행 상황이 모두 출력된 후 우승자를 출력한다.', () => {
clickAfterTypeCar();
clickAfterTypeTryCount();

cy.wait(DEFAULT_TRY_COUNT_TIME);
cy.get('.result-container').should('be.visible');

cy.document().then(doc => {
Expand All @@ -114,11 +134,26 @@ describe('자동차 경주', () => {
});
});

it('다시 시작하기 버튼 클릭 시 게임이 리셋된다', () => {
it('우승자 출력 후 2초 후에 축하의 alert 메시지를 띄운다.', () => {
clickAfterTypeCar();
clickAfterTypeTryCount();
cy.get('.restart-btn').click();

cy.wait(DEFAULT_TRY_COUNT_TIME);

const alertStub = cy.stub();
cy.on('window:alert', alertStub);
cy.wait(DELAY.WAIT_ALERT_SEC).then(() => {
expect(alertStub.getCall(0)).to.be.calledWith('축하합니다 🎉');
});
})

it('다시 시작하기 버튼 클릭 시 게임이 리셋된다.', () => {
clickAfterTypeCar();
clickAfterTypeTryCount();

cy.wait(DEFAULT_TRY_COUNT_TIME);

cy.get('.restart-btn').click();
resetUI();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 resetUI()를 호출하면 테스트하는 의미가 사라지지 않을까요? 🤔

});
});
2 changes: 2 additions & 0 deletions src/css/ui/spinner.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.spinner-container {
width: 25px;
height: 25px;
position: relative;
margin: 10px auto;
}

.spinner::after {
Expand Down
11 changes: 11 additions & 0 deletions src/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@ export const CLASS_NAMES = {
PROGRESS_CARS: '.progress-cars',
RESULT_CONTAINER: '.result-container',
RESTART_BTN: '.restart-btn',
SPINNER_CONTAINER: '.spinner-container',
FORWARD_ICON: '.forward-icon'
};

export const DELAY = {
SPINNER_SEC: 1000,
ARROW_DISPLAYING_SEC: 1000,
get TURN_TIME() {
return this.SPINNER_SEC + this.ARROW_DISPLAYING_SEC;
},
WAIT_ALERT_SEC: 2000,
}
32 changes: 22 additions & 10 deletions src/js/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Car from './models/Car.js';
import RacingUI from './racingUI.js';
import { ALERT_MESSAGES, CLASS_NAMES } from './constants.js';
import { ALERT_MESSAGES, CLASS_NAMES, DELAY } from './constants.js';
import {
isCarNameFilled,
isCarNameUnderFive,
Expand Down Expand Up @@ -61,30 +61,42 @@ export default class Racing {
};

startRace = () => {
this.UIController.showResult(this.cars);
this.moveCars();
this.getWinners();
};

moveCars = () => {
for (let i = 0; i < this.tryCount; i++) {
this.cars.forEach(car => car.move());
}
this.UIController.showProgress(this.cars);
const moveEverySecond = (interval) => {
if (this.tryCount === 0) {
interval && clearInterval(interval)
this.getWinners();
return;
}
this.tryCount--;
this.cars.forEach(car => {
car.move();
const isCarMoved = car.isMoved;
this.UIController.printProgress(car, isCarMoved);
})
};

moveEverySecond();
const moveInterval = setInterval(() => moveEverySecond(moveInterval), DELAY.TURN_TIME);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const delay = ms =>
  new Promise(resolve => {
    setTimeout(resolve, ms);
  });

와 같은 delay 유틸을 구현해줘도 좋을 것 같습니다!

};

getWinners = () => {
const sortedCars = this.cars.sort((a, b) => {
return b.getPosition() - a.getPosition();
return b.position - a.position;
})

let maxPosition = sortedCars[0].getPosition();
let maxPosition = sortedCars[0].position;
for (let car of this.cars) {
if (car.getPosition() === maxPosition) {
if (car.position === maxPosition) {
car.wins();
}
}

const winners = this.cars.filter(car => car.getIsWinner()).map(car => car.getName());
const winners = this.cars.filter(car => car.isWinner).map(car => car.name);
Comment on lines +89 to +99
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const maxPosition = Math.max(...this.cars.map((car) => car.position));

return this.cars
    .filter((car) => car.position === maxPosition)
    .map((car) => car.name);

this.UIController.showWinners(winners);
document.querySelector(CLASS_NAMES.RESTART_BTN).addEventListener('click', this.restartGame);
};
Expand Down
32 changes: 20 additions & 12 deletions src/js/models/Car.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
export default class Car {
constructor(name) {
this.name = name;
this.position = 0;
this.isWinner = false;
this._name = name;
this._position = 0;
this._isWinner = false;
this._isMoving = false;
}

getName() {
return this.name;
get name() {
return this._name;
}

getPosition() {
return this.position;
get position() {
return this._position;
}

getIsWinner() {
return this.isWinner;
get isWinner() {
return this._isWinner;
}
Comment on lines +9 to 19
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 멤버변수들에 대해서도 getter로 변경하고, 중복된 이름을 방지하기 위해 변수명에는 언더바를 붙였습니다.
이렇게 써도 괜찮을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은것 같습니다! 멤버변수에 _를 붙이는 건 많은 팀에서 하는 코딩 컨벤션 중 하나입니다!


move() {
const randNumber = Math.random() * 10;
if (randNumber >= 4) {
this.position++;
this._position++;
this._isMoving = true;
} else {
this._isMoving = false;
}
}

get isMoved() {
return this._isMoving;
}

wins() {
this.isWinner = true;
this._isWinner = true;
}
Comment on lines +31 to 37
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Car 모델이 너무 많은 역할을 하고 있습니다. movewin의 여부는 바깥의 로직에서 처리해보는 것도 좋을 것 같습니다!

}
54 changes: 42 additions & 12 deletions src/js/racingUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,24 @@ export default class RacingUI {
this.clearInput(CLASS_NAMES.TRY_COUNT);
}

showElement(className) {
if (!document.querySelector(className)) {
return;
}
const allElements = document.querySelectorAll(className)
for (let i = 0; i < allElements.length; i++) {
allElements[i].style.display = '';
}
Comment on lines +22 to +24
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...Array(allElements.length)].map(() => ...)

}

hideElement(className) {
document.querySelector(className).style.display = 'none';
if (!document.querySelector(className)) {
return;
}
Comment on lines +28 to +30
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선택자가 없어서 발생하는 에러의 경우 그냥 return하지 말고 throw Error를 이용하여 개발자 콘솔에 로그를 남기면 디버깅하기 좋습니다 👾

const allElements = document.querySelectorAll(className)
for (let i = 0; i < allElements.length; i++) {
allElements[i].style.display = 'none';
Comment on lines +32 to +33
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...Array(allElements.length)].map(() => ...)

}
}

clearText(className) {
Expand All @@ -26,26 +42,36 @@ export default class RacingUI {
document.querySelector(className).value = '';
}

showElement(className) {
if (!document.querySelector(className)) {
return;
}
document.querySelector(className).style.display = '';
}

showProgress(cars) {
showResult(cars) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실시간으로 결과를 출력하는 printProgress와 구분하기 위해 함수 이름을 바꿨습니다.
showResult 안에서는 spinner, arrow와 상관 없는 car들의 이름과 그 컨테이너 객체만 뜨도록 했습니다.

this.showElement(CLASS_NAMES.PROGRESS_CONTAINER);

document.querySelector(CLASS_NAMES.PROGRESS_CARS).innerHTML
= cars.map(car => `
<div>
<div class="car-player mr-2">${car.getName()}</div>
${`<div class="forward-icon mt-2">⬇️️</div>`.repeat(car.getPosition())}
<div class="car-player-container">
<div id="${car.name}" class="car-player mr-2">${car.name}</div>
<div class="spinner-container">
<div class="material spinner"></div>
</div>
</div>
`,
).join('');
}

printProgress(car, isCarMoved) {
this.showElement(CLASS_NAMES.SPINNER_CONTAINER, true);

const carElement = document.querySelector(`#${car.name}`);

setTimeout(() => {
this.hideElement(CLASS_NAMES.SPINNER_CONTAINER, false);
if (isCarMoved) {
carElement.insertAdjacentHTML('afterend', `
<div class="forward-icon mt-2">⬇️️</div>
`)
}
}, 1000)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매직넘버 대신 상수로 관리해주세요 😮

}

showWinners(winners) {
this.showElement(CLASS_NAMES.RESULT_CONTAINER);

Expand All @@ -57,5 +83,9 @@ export default class RacingUI {
</div>
</section>
`;

setTimeout(() => {
alert('축하합니다 🎉')
}, 2000);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매직넘버 대신 상수로 관리해주세요 222 😮

}
}