JavaScript의 주차장 관리 시스템

Oct 20 2020

가설

다음과 같은 요금의 유료 주차장이 있습니다. $1 for the first hour and $이후 1 시간마다 0.5. 주차 공간은 10 대입니다.

필수

다음과 같은 특성을 가진 주차 활동 관리 시스템을 만드십시오.

  • 주차 간격은 가장 가까운 시간으로 반올림됩니다.
  • 신차가 주차장에 들어 오면 주차 시스템은 차량의 등록 번호를받습니다.
  • 차량이 주차를 떠나면 시스템은 체류 기간에 따라 요약을 발행합니다. 요약의 일부로 중요하다고 생각하는 정보는 무엇입니까?
  • 모든 고객은 지정된 시간에 주차장에 주차 된 차량 목록을 볼 수 있습니다.

기술 요구 사항

  • 구현은 JavaScript를 사용하여 수행되며 모든 데이터는 메모리에 저장되며 영구 저장이 필요하지 않습니다.
  • 깨끗한 코드를 작성하십시오.
  • 컴퓨터에서 데이터를 입력하고 읽을 수있는 간단한 GUI를 만듭니다. 같은 페이지에 데이터를 표시합니다.

해결책

저는 미학을 위해 Bootstrap 4를 사용했습니다. 스크립트 자체는 JavaScript 만 사용합니다. jQuery는 Bootstrap의 corect 기능을 위해 포함됩니다.

let cars = [];
const addCarButton = document.querySelector('#carButton');
const minLicenseeLength = 7;
const payPerHour = 0.5;
const payFirstHour = 1;
const totalPlaces = 10;

const formatDate = (date) => {
  var hours = date.getHours();
  var minutes = date.getMinutes();
  var ampm = hours >= 12 ? 'PM' : 'AM';
  hours = hours % 12;
  hours = hours ? hours : 12;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  var strTime = hours + ':' + minutes + ' ' + ampm;
  return date.getDate() + "/" + (date.getMonth() + 1) + "/" + date.getFullYear() + " " + strTime;
}

const secondsToHours = (d) => {
  d = Number(d);
  let h = Math.ceil(d / 3600);
  return h;
}

const renterTable = () => {
  let results = '';
  for (var i = 0; i < cars.length; i++) {
    let licensee = cars[i].licensee;
    let arrival = formatDate(cars[i].arrival);
    let leave = cars[i].leave === '-' ? '-' : formatDate(cars[i].leave);

    results += `<tr>
          <td>${licensee}</td> <td>${arrival}</td>
          <td>${leave}</td> <td>${showStatus(cars[i])}</td>
          <td class="text-right">${makeBill(cars[i])}</td> <td class="text-right"> <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                    </td>
        </tr>`;
  }
  document.querySelector("#parking tbody").innerHTML = results;
}

const showStatus = (car) => {
  return car.isParked ? "Parked" : "Has left";
}

const changeStatus = (event) => {
  cars[event.target.dataset.row].isParked = false;
}

const setLeaveTime = (event) => {
  cars[event.target.dataset.row].leave = new Date(Date.now());
}

const countAvailablePlaces = (event) => {
  document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;
}

const setClassForBadge = () => {
  let badgeClassName = cars.length == totalPlaces ? 'badge badge-danger' : 'badge badge-success';
  document.querySelector('#placesCount').setAttribute('class', badgeClassName);
}

const calculateHoursBilled = (car) => {
  let arrivedAt = new Date(car.arrival).getTime();
  let leftAt = new Date(car.leave).getTime();
  return secondsToHours((leftAt - arrivedAt) / 1000); //duration in seconds
}

const makeBill = (car) => {
  let hoursBilled = calculateHoursBilled(car);
  let billValue = car.isParked ? "-" : "$" + (payFirstHour + (hoursBilled - 1) * payPerHour); return billValue; } const printSummary = (event) => { let car = cars[event.target.dataset.row]; let sumarryTable = `<table class="table table-bordered m-0"> <tr> <td class="font-weight-bold">Registration number</td> <td>${car.licensee}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Arrival</td>
          <td>${formatDate(car.arrival)}</td> </tr> <tr> <td class="font-weight-bold">Departure</td> <td>${formatDate(car.leave)}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Billable hours</td>
          <td>${calculateHoursBilled(car)}</td> </tr> <tr> <td class="font-weight-bold">Bill value</td> <td>${makeBill(car)}</td>
      </tr></table>`;

  document.querySelector('#modalBody').innerHTML = sumarryTable;
}

const showSummary = (event) => {
  changeStatus(event);
  setLeaveTime(event);
  renterTable();
  printSummary(event);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(event);
  }, 3000);
}

const addCar = () => {
  let newLicensee = document.querySelector("#carValue").value;

  let newCar = {
    licensee: newLicensee,
    arrival: new Date(),
    leave: '-',
    isParked: true
  }

  // Add new car to the cars array

  document.querySelector('#message').style.display = 'none';
  if (newLicensee.length >= minLicenseeLength && cars.length < totalPlaces) {
    cars.unshift(newCar);
  } else {
    if (newLicensee.length < minLicenseeLength) {
      document.querySelector('#message').style.display = 'block';
    }
  }

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled', true);
  }

  setClassForBadge();

  //Update places count
  countAvailablePlaces(event);

  // Empty text box
  document.querySelector("#carValue").value = '';

  // Render the table
  renterTable();
}

const freeSpot = (event) => {
  cars.splice(event.target.dataset.row, 1);
  setClassForBadge();

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled');
  } else {
    document.querySelector('#carButton').removeAttribute('disabled');
  }

  // Render Table again after delete 
  renterTable();
  //Update places count
  countAvailablePlaces(event);
}

// Add new car to the array
addCarButton.addEventListener('click', addCar);

// Render Table
renterTable();

//Show places count at page load
countAvailablePlaces(event);
#addForm {
  position: relative;
}

#message {
  display: none;
  position: relative;
  font-size: 10px;
  position: absolute;
}

#placesCount {
  font-size: 90%;
}

#parking th,
#parking td {
  white-space: nowrap;
  font-size: 14px;
}

#myModal .font-weight-bold {
  font-weight: 500 !important;
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

<div class="container">
  <div class="card my-3">
    <div class="card-header px-3 d-flex">
      <h5 class="m-0">Parking management</h5>
      <div class="ml-auto">
        <span id="placesCount" class="badge badge-success"></span> available places
      </div>
    </div>
    <div class="card-body p-0">
      <div class="input-group p-2" id="addForm">
        <input type="text" class="form-control" id="carValue" placeholder="Registration number">
        <div class="input-group-append">
          <button id="carButton" class="btn btn-sm btn-success">Park car</button>
        </div>
        <p id="message" class="text-danger m-0">Registration number invalid</p>
      </div>
      <div class="table-responsive">
        <table id="parking" class="table table-striped m-0">
          <thead>
            <tr>
              <th>Registration no</th>
              <th>Arrival</th>
              <th>Departure</th>
              <th>Status</th>
              <th class="text-right">Bill</th>
              <th class="text-right">Actions</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<div class="modal" id="myModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h6 class="modal-title">Sumar Parcare</h6>
        <button type="button" class="close" data-dismiss="modal">&times;</button>
      </div>
      <div id="modalBody" class="modal-body"></div>
    </div>
  </div>
</div>

보안 관점을 포함하여이 애플리케이션에 문제가 있습니까?

감사!

답변

5 CertainPerformance Oct 21 2020 at 00:50

선호const 이상 let하고 var. 를 사용하여 변수를 선언 let하면 코드를 읽는 모든 사람에게 변수가 언제든지 다시 할당 될 수 있음을 나타냅니다. 코드가 작동하는 데 실제로 재 할당이 필요하지 않은 경우 이는 바람직하지 않은인지 오버 헤드를 초래합니다. "이 변수 이름은 언제든지 재 할당 될 수 있으므로 지금 무엇을 의미하는지 원래 할당 된 것과 다를 수 있습니다. * 참조https://softwareengineering.stackexchange.com/questions/278652/how-much-should-i-be-using-let-vs-const-in-es6

var완전히 피하는 것이 가장 좋은 것은 오늘날 사용할 가치가있는 문제가 너무 많다는 것입니다. 가장 중요한 것은 블록 범위 대신 직관적이지 않은 기능 범위가 있다는 것입니다.

문자열 패딩은 당신이 문자열을 특정 길이를 (예를 들어, 여기에, 수 있도록 패드에 몇 가지 주요 문자가있는 문자열의 시작을하려는 경우 9100910각각), 사용하기에 가장 적절한 방법이다padStart

템플릿 리터럴 은 여러 변수를 문자열로 보간해야 할 때 유용합니다. 종종 스타일보다 선호됩니다 ' + someVar + '.

위의 3 가지 팁을 고려하면 formatDate함수를 다음과 같이 리팩토링 할 수 있습니다.

const formatDate = (date) => {
  const hoursMilitary = date.getHours();
  const minutesToDisplay = String(date.getMinutes()).padStart('0', 2);
  const ampm = hoursMilitary >= 12 ? 'PM' : 'AM';
  const hoursToDisplay = (hoursMilitary % 12) || 12;
  const strTime = `${hoursToDisplay}:${minutesToDisplay} ${ampm}`; return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${strTime}`;
}

secondsToHours 는 매개 변수를 재할 당하지 않고도 훨씬 더 간결하게 만들 수 있습니다.

const secondsToHours = d => Math.ceil(d / 3600);

/숫자가 아닌 숫자를 숫자로 강제합니다. 당신이 호출 할 필요는 없습니다 Numberd처음.

renterTable여기에서 많은 개선을 할 수 있습니다. 첫째, 함수 이름은 완전히 직관적이지 않습니다. renterTable(명사) 라는 것은 무엇을 합니까? HTMLTableElement를 포함하는 변수입니까? 테이블을 생성하고 반환합니까? 아니요, cars배열 의 데이터가 주어지면 DOM에서 테이블을 다시 렌더링합니다 . 대신 renderTable또는 renderRenterTable(동사) 라고 부르는 것을 고려하십시오 .

사용자 입력을 연결하여 HTML을 구성하는 것은 안전하지 않습니다 .

  results += `<tr>
        <td>${licensee}</td>
        <td>${arrival}</td> <td>${leave}</td>
        <td>${showStatus(cars[i])}</td> <td class="text-right">${makeBill(cars[i])}</td>
        <td class="text-right">
          <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                  </td>
      </tr>`;
}
document.querySelector("#parking tbody").innerHTML = results;

이것은 임의의 코드 실행을 허용합니다. 누군가가 다음과 같이 말했다고 상상해보십시오.

자동차를 등록 할 때 다음 번호판을 연결해보십시오. 다음에 무슨 일이 일어나는지 믿기지 않을 것입니다!

<img src onerror='alert("evil")'>

어디에 alert("evil")무엇으로 대체 할 수있는 피셔는 원한다. 그런 다음 너무 지능적이지 않은 일부 사용자는 그것에 빠져 로그인 정보 / 계좌 자금 등이 손상 될 수 있습니다.

이 문제는 licensee먼저에 유효한 문자 만 포함 하도록하여 해결할 수 있습니다 .

let newLicensee = document.querySelector("#carValue").value;
if (!/^[a-z\d]+$/.test(newLicensee)) {
  // display error message to user: license invalid
  return;
}

그러나 그것은 여전히 ​​패치 작업 일뿐입니다. 나중에 스크립트가 개발 되면 DOM에 삽입하기 전에 실수 로 입력을 삭제 하지 않는 것이 두렵 습니다. 사용자 입력이 관련되면 HTML로 문자열 보간을 완전히 피하고 대신 textContent요소가 문서에 삽입 된 후 요소에 삽입하는 것을 선호합니다 . 예 :

const tr = document.querySelector("#parking tbody").appendChild(document.createElement('tr'));
tr.innerHTML = `
  <td></td>
  <td></td>
  <td></td>
  ...
`;
tr.children[0].textContent = licensee;
tr.children[1].textContent = arrival;
tr.children[2].textContent = leave;
// ...

또는 더 큰 프로젝트의 경우 React와 같이 DOM에 입력을 간결하게 보간 할 수있는 프레임 워크를 사용하는 것이 좋습니다 .

const CarRow = (carInfo) => (
  <tr>
    <td>{carInfo.licensee}</td>
    <td>{carInfo.arrival}</td>
    <td>{carInfo.leave}</td>
    ...

위의 모든 내용은 sumarryTable동일한 종류의 연결된 HTML 취약성을 가진 에도 적용됩니다.

맞춤법 맞춤법은 프로그래밍에서 중요합니다. 올바른 맞춤법 변수 이름은 버그를 방지하는 데 도움이됩니다. summaryTable대신 전화하십시오 . 또한 "라이선스"라는 단어는 면허를 보유한 사람을 나타내지 만 변수는 번호판 문자열을 나타냅니다. licenseID대신 호출 해보십시오 licensee. 덜 혼란 스러울 것입니다.

innerHTML 위와 비슷한 이유로 만 사용 innerHTML하면 삽입 HTML 마크 업에 필요로 할 때. 요소에 텍스트 를 삽입 하려면 textContent; 더 안전하고 빠르며 HTML에서 특별한 의미를 가진 문자에는 문제가 없습니다. 이:

document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;

될 수 있습니다

document.querySelector('#placesCount').textContent = totalPlaces - cars.length;

( #placesCount함수를 호출 할 때마다 다시 선택하는 대신 참조를 저장할 수도 있습니다. )

인라인 처리기를 사용하지 마십시오 . 행 마크 업에는 다음이 있습니다.

<button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>

인라인 핸들러에는 현대 코드에서 사용할 가치가없는 문제 가 너무 많습니다 . 그들은 세계적 오염을 필요로하고 다른 문제들 중에서도 미친 범위 체인을 가지고 있습니다. 대신 JavaScript를 사용하여 이벤트 리스너를 올바르게 연결하십시오. 버튼이 DOM에 삽입 될 때 각 버튼에 리스너를 연결하거나 더 나은 옵션은 이벤트 위임을 사용하는 것입니다. 아래 주석도 참조하십시오.

table.addEventListener('click', (event) => {
  if (!event.target.matches('button')) {
    return;
  }
  // I renamed this from "row" to "rowIndex" to make the variable name more precise;
  // it's not an actual row, it's only an index
  const { rowIndex } = event.target.dataset;
  // Now that the rowIndex has been extracted, grab the car,
  // then pass the car along instead of extracting it inside each function:
  const car = cars[rowIndex];
  changeStatus(car);
  setLeaveTime(car);
  
  renderRenterTable();
  printSummary(car);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(rowIndex);
  }, 3000);
});

가능하면 점 표기법을 선호 하십시오. 더 간결하고 읽기 쉽습니다.

document.querySelector('#placesCount').setAttribute('class', badgeClassName);
// ...
if (cars.length == totalPlaces) {
  document.querySelector('#carButton').setAttribute('disabled');
} else {
  document.querySelector('#carButton').removeAttribute('disabled');
}

다음과 같이 변경할 수 있습니다.

document.querySelector('#placesCount').className = badgeClassName;
// ...
document.querySelector('#carButton').disabled = cars.length === totalPlaces;

( === 대신 항상 사용 하는 것을 기억하십시오 ==; ==구현되는 논리를 파악하기 위해 스크립트 독자가 이해할 필요가없는 이상한 규칙이 있습니다)

현대적인 구문 현대적인 구문과 현대적인 방법이 소스 코드에서 사용되고 있습니다. 이것은 깔끔하고 간결하며 읽기 쉬운 코드를 만듭니다. 프로덕션 코드의 경우 Babel 및 polyfill을 사용 하여 소스를 ES5로 자동 트랜스 파일하여 IE와 같은 오래된 브라우저에서도 코드를 실행할 수 있습니다.