[TS 과제 챌린지] Movie Seat Booking - 구현

조민호·2023년 5월 17일
0

TS스터디 팀원들과 함께 매주 주차별 과제를 진행합니다


https://github.com/bradtraversy/vanillawebprojects

  • 위의 링크에는 바닐라JS로 간단히 구현된 단일 페이지의 예시들이 있습니다
  • HTML/CSS는 기존에 작성된 예시를 그대로 사용하지만, JS로직은 전부 삭제하고 스스로의 로직대로 완전히 새로 작성합니다
    - 이때 , 모든 기능 구현을 JS대신 TS로 직접 변환해서 작성합니다


1주차 - Movie Seat Booking

기능 동작 예시 링크 : https://vanillawebprojects.com/projects/movie-seat-booking/


구현 코드

const container = document.querySelector('.container') as HTMLDivElement;
const seats = document.querySelectorAll(
  '.row .seat:not(.occupied)',
) as NodeListOf<Element>;
const count = document.getElementById('count') as HTMLSpanElement;
const total = document.getElementById('total') as HTMLSpanElement;
const movieSelect = document.getElementById('movie') as HTMLSelectElement;

type initialSettingFunc = () => void;
type changeTextFunc = () => void;
type updateSeatsFunc = () => void;

type seatIndex = {
  seatNum: number[];
};
let seatStatus: seatIndex = {
  seatNum: [],
};

type movieInfo = {
  movieIndex: number;
  moviePirce: number;
};
let movieStatus: movieInfo = {
  movieIndex: movieSelect.selectedIndex,
  moviePirce: Number(movieSelect.value),
};

// 브라우저 하단에 배치된 선택 좌석 갯수 , 총 금액 메세지 수정 메소드
const changeText: changeTextFunc = () => {
  let status: seatIndex = JSON.parse(localStorage.getItem('selectedSeats')!);
  let currentSeatsCount: number = status.seatNum.length;
  count.innerText = String(currentSeatsCount);
  total.innerText = String(currentSeatsCount * movieStatus.moviePirce!);
};

const initialSetting: initialSettingFunc = () => {
  let arr: number[] = [];
  seats.forEach((i, index) => {
    arr.push(index);
  });

  let localStorageResult: string | null;

  // 좌석 불러오기
  localStorageResult = localStorage.getItem('selectedSeats');

  // 브라우저 최초 실행 시
  if (localStorageResult === null) {
    localStorage.setItem('selectedSeats', JSON.stringify(seatStatus));
  }
  if (localStorageResult !== null) {
    let status: seatIndex = JSON.parse(localStorageResult);

    // 상태 변경
    seatStatus = { ...status };

    // css변경을 위한 클래스명 추가
    seatStatus.seatNum.forEach((i) => {
      if (arr.includes(i)) {
        seats[i].classList.add('selected');
      }
    });
  }

  // 선택된 영화 불러오기
  localStorageResult = localStorage.getItem('selectedMovieInfo');

  // 브라우저 최초 실행 시
  if (localStorageResult === null) {
    localStorage.setItem('selectedMovieInfo', JSON.stringify(movieStatus));
  }
  if (localStorageResult !== null) {
    let status: movieInfo = JSON.parse(localStorageResult);

    // 상태 변경
    movieStatus = { ...status };

    // select태그 값 변경
    movieSelect.selectedIndex = movieStatus.movieIndex;

    // 하단 메세지 수정
    changeText();
  }
};

// 좌석 업데이트 메소드
const updateSeats: updateSeatsFunc = () => {
  const newSelectedNodes: Element[] = Array.from(
    document.querySelectorAll('.row .seat.selected') as NodeListOf<Element>,
  );

  let arr: number[] = newSelectedNodes.map((i) => {
    return Array.from(seats).indexOf(i);
  });

  // 상태 변경
  let status: seatIndex = {
    seatNum: arr,
  };
  seatStatus = { ...status };

  // 변경된 상태를 로컬스토리지에 등록
  localStorage.setItem('selectedSeats', JSON.stringify(seatStatus));

  changeText();
};

movieSelect.addEventListener('change', (e: Event) => {
  let price: number = Number((e.target as HTMLSelectElement).value);
  let index: number = Number((e.target as HTMLSelectElement).selectedIndex);

  let status: movieInfo = {
    movieIndex: index,
    moviePirce: price,
  };
  movieStatus = { ...status };
  localStorage.setItem('selectedMovieInfo', JSON.stringify(movieStatus));

  changeText();
});

// Seat click event
container.addEventListener('click', (e: Event) => {
  if (
    (e.target as HTMLDivElement).classList.contains('seat') &&
    !(e.target as HTMLDivElement).classList.contains('occupied')
  ) {
    (e.target as HTMLDivElement).classList.toggle('selected');

    updateSeats();
  }
});

initialSetting();

설명

이 로직은 좌석을 인덱스로 사용합니다

위의 사진처럼 div태그는 현재 48개가 존재하며

선택된(하늘색 좌석)좌석은 각각 0,1,3,5번째 div요소가 되는 것입니다




최상단에서 상태 값을 가지게 되며 상태값이 갱신 될 때마다 로컬스토리지도 갱신합니다

  1. 상태값을 기반으로 영화 선택 , 총 좌석 확인 , 금액 계산 등의 작업을 진행합니다

  2. 상태값을 기반으로 위와 같은 작업을 진행한다면 ,

    로컬스토리지는 브라우저가 실행 될 때마다 로컬스토리지의 값으로 초기 상태를 업데이트 하고 이 상태를 기반으로 초기 셋팅을 진행하게 됩니다
    (ex 마지막으로 선택했던 좌석과 영화 상태를 그대로 사용)

즉, 각종 이벤트에 대한 로직은 상태값을 기반으로 , 브라우저의 재 실행 시 데이터 보존은 로컬스토리지를 기반으로 진행하여 상태와 로컬스토리지의 사용처를 분리합니다

  • 선택된 좌석 인덱스를 가지는 상태 seatStatus
    type seatIndex = {
      seatNum: number[];
    };
    let seatStatus: seatIndex = {
      seatNum: [],
    };
  • 선택된 영화 정보 인덱스를 가지는 상태 movieStatus
    type movieInfo = {
      movieIndex: number;
      moviePirce: number;
    };
    let movieStatus: movieInfo = {
      movieIndex: movieSelect.selectedIndex,
      moviePirce: Number(movieSelect.value),
    };



브라우저 하단에 배치된 메세지를 수정하는 메소드입니다

const changeText: changeTextFunc = () => {
  let status: seatIndex = JSON.parse(localStorage.getItem('selectedSeats')!);
  let currentSeatsCount: number = status.seatNum.length;
  count.innerText = String(currentSeatsCount);
  total.innerText = String(currentSeatsCount * movieStatus.moviePirce!);
};
  • 로컬스토리지에서 좌석 정보를 가져옵니다

    로직상 이때 호출하는 로컬스토리지의 selectedSeats값은
    (빈 배열이라 하더라도)무조건 null이 아니기에 Non-null Assertion을 사용 했습니다

  • 좌석 정보로 상태값의 배열 길이가 현재 선택된 좌석 갯수입니다

  • 그러므로 현재 선택된 좌석 갯수 , 현재 선택된 영화 상태를 기반으로 메세지를 수정합니다




브라우저가 실행 됐을 때 , 초기셋팅을 진행하는 메소드입니다

const initialSetting: initialSettingFunc = () => {

  // occupied가 아닌 좌석들의 인덱스를 가지는 배열
  let nonoOccupied: number[] = [];
  seats.forEach((i, index) => {

    //i는 div요소를 나타내고 이 로직에서는 div요소의 인덱스가 좌석 번호를 의미함
    nonoOccupied.push(index);
  });

  let localStorageResult: string | null;

  // 좌석 불러오기
  localStorageResult = localStorage.getItem('selectedSeats');

  // 브라우저 최초 실행 시
  if (localStorageResult === null) {
    localStorage.setItem('selectedSeats', JSON.stringify(seatStatus));
  }
  if (localStorageResult !== null) {
    let status: seatIndex = JSON.parse(localStorageResult);

    // 상태 업데이트
    seatStatus = { ...status };

    // css변경을 위한 클래스명 추가
    seatStatus.seatNum.forEach((i) => {
      if (nonoOccupied.includes(i)) {
        seats[i].classList.add('selected');
      }
    });
  }

  // 선택된 영화 불러오기
  localStorageResult = localStorage.getItem('selectedMovieInfo');

  // 브라우저 최초 실행 시
  if (localStorageResult === null) {
    localStorage.setItem('selectedMovieInfo', JSON.stringify(movieStatus));
  }
  if (localStorageResult !== null) {
    let status: movieInfo = JSON.parse(localStorageResult);

    // 상태 업데이트
    movieStatus = { ...status };

    // select요소의 선택 인덱스 변경
    // 브라우저 재 실행시, select의 값이 바뀌어져 있는 경우가 있으므로
    // movieStatus의 값을 기반으로 변경해 줘야 함
    movieSelect.selectedIndex = movieStatus.movieIndex;

    // 하단 메세지 수정
    changeText();
  }
};
  • 선택됐던 영화 좌석과 영화 정보 상태를 불러옵니다
    • 로컬스토리지에서 불러오는데, 만약 null 값이라면 아예 로컬스토리지 자체에 등록이 안 되어 있는 것이라면 이 애플리케이션 자체를 아예 처음 실행한 것이므로 로컬스토리지에 상태값을 등록해줍니다

      TS에서 로컬스트리지를 사용할 시에 이러한 방법으로 **무조건 null 을 막아줘야 합니다**

      💡 이때 상태값은 최초의 상태이므로 영화좌석은 빈 배열, 영화정보는 첫번째 select태그의 정보를 넣습니다
    • null 값이 아니라면 이전 해당 어플리케이션을 사용했다는 것입니다 그러므로 불러온 값을 바탕으로 현재 상태를 업데이트 해주고
      1. 영화 좌석의 경우 , 상태를 바탕으로 선택된 좌석 태그에 대해 selected클래스를 추가해서 css 효과를 부여하고
      2. 영화 정보의 경우 select요소의 선택 인덱스를 변경해 줍니다
  • 그리고 하단 메세지를 수정하는 메소드를 호출합니다 ( 로컬스토리지 값이 null인 경우에는 어차피 최초 실행이므로
    선택된 좌석이 있을리가 없으므로 changeText();를 null이 아닌 경우에만 호출해도 무방합니다 )




좌석 업데이트 메소드입니다

const updateSeats: updateSeatsFunc = () => {
  const newSelectedNodes: Element[] = Array.from(
    document.querySelectorAll('.row .seat.selected') as NodeListOf<Element>,
  );

  let arr: number[] = newSelectedNodes.map((i) => {
    return Array.from(seats).indexOf(i);
  });

  // 상태 업데이트
  let status: seatIndex = {
    seatNum: arr,
  };
  seatStatus = { ...status };

  // 변경된 상태를 로컬스토리지에 등록
  localStorage.setItem('selectedSeats', JSON.stringify(seatStatus));

  changeText();
};
  • 마우스로 좌석을 선택하게 되면 발생하는 메소드입니다

  • .selected클래스가 토글 된 모든 div요소들을 가져와서

    상태를 업데이트하고 , 변경된 상태를 로컬스토리지에 등록합니다

    이제 어플리케이션을 종료하고 다시 실행해도 이 로컬스토리지 값을 기반으로 이어서 진행하게 됩니다

    ( css는 이미 .selected클래스가 토글 된 순간 사라지거나 추가되기 때문에 여기서 다루지 않습니다 )

  • 그리고 changeText()를 호출해서 하단 텍스트를 수정합니다




영화 선택 이벤트 입니다

movieSelect.addEventListener('change', (e: Event) => {
  let price: number = Number((e.target as HTMLSelectElement).value);
  let index: number = Number((e.target as HTMLSelectElement).selectedIndex);

  let status: movieInfo = {
    movieIndex: index,
    moviePirce: price,
  };
  movieStatus = { ...status };
  localStorage.setItem('selectedMovieInfo', JSON.stringify(movieStatus));

  changeText();
});
  • select태그에서 영화선택을 하게 되면 이 값을 기반으로 상태값을 수정하고 로컬스토리지에 갱신 한 뒤
  • changeText()로 하단 메세지를 수정합니다




좌석 선택 이벤트 입니다

container.addEventListener('click', (e: Event) => {
  if (
    (e.target as HTMLDivElement).classList.contains('seat') &&
    !(e.target as HTMLDivElement).classList.contains('occupied')
  ) {
    (e.target as HTMLDivElement).classList.toggle('selected');

    updateSeats();
  }
});
  • 선택한 좌석이 .seat이며 .occupied가 아닌 요소이면 .selected를 토글해주고
  • updateSeats() 메소드를 호출합니다
profile
할 수 있다

0개의 댓글