dh_0e

[Project] 영화 검색 사이트 심화 (팀 프로젝트) 본문

내일배움캠프/Project

[Project] 영화 검색 사이트 심화 (팀 프로젝트)

dh_0e 2024. 5. 12. 23:44

 내배캠 3번째 프로젝트는 팀 프로젝트로 이 전에 개인 프로젝트를 팀원들끼리 공유하여 한 프로젝트를 선정하여 발전 시키는 것이다. 우리 팀에선 디자인은 평범하지만 코드가 깔끔한 내 작업물로 진행하기로 했으며, 자연스럽게 내가 팀장을 맡게 되었다. 팀장 역할은 처음이었으며, 프로젝트에 대한 책임감이 많이 올라가 더욱 열심히 했던 것 같다.

프로젝트에 추가해야될 필수 요구사항과 선택 요구사항은 다음과 같다.

더보기
  • 필수요구사항
    • [1] TMDB 또는 영화진흥위원회 오픈 API 이용(택 1 또는 중복 사용)영화진흥위원회 오픈API
      • TMDB, 영화진흥위원회 오픈 API 모두 사용 가능합니다.
      • 둘 중 한가지만 사용하여도 무방합니다.
      • 기존에 TMDB를 이용해 작성한 과제에 어떻게 적용해야 하나요?
        • 안1) 기존 과제의 UI 및 API를 그대로 사용합니다(영화진흥위원회 오픈 API 미사용)
        • 안2) 기존 과제의 UI는 유지하되, API만 변경할 수 있습니다.
        • 안3) 기존 과제의 UI를 사용하려는 API 환경에 맞게 새롭게 구성합니다.
    •  💡 [영화진흥위원회 오픈 API란?] 영화진흥위원회 ‘**영화관입장권통합전산망’**에서 제공하는 오픈 API입니다. ‘박스오피스’, ‘영화정보’, 영화사정보’, ‘영화인정보’등 다양한 서비스를 제공하며, 문서가 국문으로 구성되어 있어 편합니다. 자세한 내용은 아래 링크를 참고해주세요.
    • [2] 영화정보 상세 페이지 구현
      • 기존 영화정보 카드 리스트에서 특정 item을 선택할 시, 상세 페이지로 이동하도록 구현합니다.
      • 상세 페이지에서 메인 페이지(홈)로 이동하는 UI도 함께 구성합니다.
    • [3] 상세 페이지 영화 리뷰 작성 기능 구현
      • 상세페이지에서 특정 영화에 대해 의견을 작성할 수 있는 UI를 구현합니다.
        • 작성자, 리뷰, 확인비밀번호를 입력하도록 구현합니다.
      • 작성한 정보는 브라우저의 localStorage에 적재하도록 합니다.localStorage를 이용하면 웹 브라우저에서 로컬 데이터를 저장하고, 불러와 사용할 수 있습니다. 위와 같이 setItem 메소드를 사용하면 key와 value를 로컬 스토리지에 저장할 수 있습니다. 만약 이미 저장된 key 값이 있다면, 이전에 저장된 value 값을 대체합니다.위와 같이 실행하면, 'username'이라는 key에 'Alice'라는 value를 저장하게 됩니다.
        localStorage.getItem('key');
        
        위와 같이 getItem 메소드를 사용하면 key를 통해 저장된 value 값을 불러올 수 있습니다.위와 같이 실행하면, 'username'이라는 key에 해당하는 'Alice'라는 value를 읽어올 수 있습니다.
        localStorage.removeItem('key');
        
        위와 같이 removeItem 메소드를 사용하면 key 값에 해당하는 데이터를 삭제할 수 있습니다.위와 같이 실행하면, 'username'이라는 key에 해당하는 'Alice'라는 value를 삭제할 수 있습니다.
      • localStorage.setItem('key', 'value');
      • 💡 [Chrome 브라우저에서 localStorage에 저장된 정보를 확인하는 방법]
        1. Chrome 개발자 도구 열기: 이를 위해 우선 브라우저에서 F12 키를 눌러 개발자 도구를 엽니다.
        2. Application 탭에서 Storage 선택: 개발자 도구에서 Application 탭을 선택하고, 하위에 있는 Storage 항목을 클릭합니다.
        3. Local Storage 선택: 이어서 Storage 항목 아래에 있는 Local Storage 항목을 선택합니다.
        4. localStorage에 저장된 정보 확인: 선택한 Local Storage 항목에서, 왼쪽에 저장된 정보의 키(key) 목록이 표시됩니다. 이 중 확인하려는 정보의 키를 클릭하면, 오른쪽에 해당 정보의 값(value)이 표시됩니다. 값이 JSON 형태로 저장된 경우, 개발자 도구에서 자동으로 이를 파싱(parsing)하여 보여줍니다.
      •  
      • localStorage.removeItem('username');
      • (3) 데이터 삭제하기(removeItem)
      • localStorage.getItem('username');
      • (2) 데이터 불러오기(getItem)
      • localStorage.setItem('username', 'Alice');
      • (1) 데이터 저장하기(setItem)
      •  💡 [localStorage를 사용하는 방법]
    • [4] github PR(=Pull Request) 사용한 협업
    • [5] UX를 고려한 validation check
      • 댓글 작성 시
      • 비밀번호 생성
      • 추가 기능 구현 시 반드시 삽입
    • [6] 하기 기재된 Javascript 문법 요소를 이용하여 구현
      • 문법 리스트(아래 사항 중 5개 이상)
        • const와 let만을 이용한 변수 선언
        • const a = 'test 01'; let b = 'test 02'; var c = 'no way!'; //쓰지 말 것
        • 형 변환 : 하기 예시 중 2개 이상 사용
          • number → string
          • string → number
          • boolean → string
        • 연산자 : 하기 예시 중 3개 이상 사용
          • 논리곱(&&) 연산자
          • 논리합(||) 연산자
          • 논리부정(!) 연산자
          • 삼항연산자(? :)
          • 타입연산자(typeof)
        • 화살표 함수 : 하기 예시 중 2개 이상 사용
          • 일반 화살표 함수
          • let add = (x, y) => { return x + y; } console.log(add(2, 3)); // 5
          • 한 줄로 된 화살표 함수
          • let add = (x, y) => x + y; console.log(add(2, 3)); // 5
          • 매개변수가 하나인 화살표 함수
          • let square = x => x * x; console.log(square(3)); // 9
        • 조건문 : 하기 예시 전부 구현
          • if문(3개 중 1개 이상 필수)
            • if
            • if-else
            • if-else if-else
          • switch문
          • 삼항연산자
          • 조건문 중첩(2개 중 1개 이상 필수)
            • 2중 if
            • if 내부 switch
        • 반복문 : 하기 예시 전부 구현
          • for문(3개 중 2개 이상 구현)
            • 일반 for문
            • for … in문
            • for … of문
          • while문(2개 중 1개 이상 구현)
            • 일반 while문
            • do ~ while 문
          • 반복 제어 명령문(2개 중 1개 이상 구현)
            • break문
            • continue문
        • 객체
          • 객체 병합
          let person1 = {
            name: "홍길동",
            age: 30
          };
          
          let person2 = {
            gender: "남자"
          };
          
          let mergedPerson = {...person1, ...person2};
          
          console.log(mergedPerson);   // { name: "홍길동", age: 30, gender: "남자" }
          
        • 배열(1) : 하기 예시 중 2개 이상 사용
          • push
          • pop
          • shift
          • unshift
          • splice
          • slice
        • 배열(2) : 하기 예시 중 3개 이상 사용
          • forEach
          • map
          • filter
          • reduce
          • find
        • 자료구조 : 하기 예시 중 1개 이상 사용
          • Map 생성 및 사용하기
            • new Map() – 새로운 Map을 만듭니다.
            • map.set(key, value) – key를 이용해 value를 저장합니다.
            • map.get(key) – key에 해당하는 값을 반환합니다. key가 존재하지 않으면 undefined를 반환합니다.
            • map.has(key) – key가 존재하면 true, 존재하지 않으면 false를 반환합니다.
            • map.delete(key) – key에 해당하는 값을 삭제합니다.
            • map.clear() – 맵 안의 모든 요소를 제거합니다.
            • map.size – 요소의 개수를 반환합니다.
          • 💡 [Map의 주요 메서드 및 프로퍼티]
          • Set 생성 및 사용하기(반복 포함)
            • new Set() : 새로운 Set을 생성합니다.
            • set.add(value) : Set에 새로운 값을 추가합니다.
            • set.has(value) : Set에 특정 값이 존재하는지 여부를 반환합니다.
            • set.delete(value) : Set에서 특정 값을 삭제합니다.
            • set.clear() : Set 안의 모든 요소를 제거합니다.
            • set.size : Set 안의 요소 개수를 반환합니다.
          • 💡 [Set의 주요 메서드 및 프로퍼티]
        • null, undefined : 하기 예시 필수 구현
          • null과 undefined를 활용한 ‘없는 값'에 대한 처리
          • if(testValue === null) { // do something } if(testValue === undefined) { // do something }
        • callback 함수 : 하기 예시 필수구현
          • setTimeout, setInterval을 활용한 callback 함수 처리하기
        • DOM 제어하기 : 하기 api 목록 중, 4개 이상 사용하기
          • document.createElement(tagName) : 새로운 HTML 요소를 생성합니다.
          • document.getElementById(id) : id 속성을 기준으로 요소를 선택합니다.
          • document.getElementsByTagName(name) : 태그 이름을 기준으로 요소를 선택합니다.
          • document.getElementsByClassName(name) : 클래스 이름을 기준으로 요소를 선택합니다.
          • document.querySelector(selector) : CSS 선택자를 이용하여 요소를 선택합니다.
          • document.querySelectorAll(selector) : CSS 선택자를 이용하여 모든 요소를 선택합니다.
          1. 문서 객체 조작
          • element.innerHTML : 해당 요소 내부의 HTML 코드를 변경합니다.
          • element.textContent : 해당 요소 내부의 텍스트를 변경합니다.
          • element.setAttribute(attr, value) : 해당 요소의 속성 값을 변경합니다.
          • element.getAttribute(attr) : 해당 요소의 속성 값을 가져옵니다.
          • element.style.property : 해당 요소의 스타일 값을 변경합니다.
          • element.appendChild(child) : 해당 요소의 하위 요소로 child를 추가합니다.
          • element.removeChild(child) : 해당 요소의 하위 요소 중 child를 삭제합니다.
          • element.classList.add(class) : 해당 요소의 클래스에 새로운 클래스를 추가합니다.
          • element.classList.remove(class) : 해당 요소의 클래스 중에서 특정 클래스를 제거합니다.
          • element.classList.toggle(class) : 해당 요소의 클래스 중에서 특정 클래스를 추가 또는 제거합니다.
          1. 이벤트 처리
          • element.addEventListener(type, listener) : 해당 요소에서 이벤트가 발생했을 때 호출할 함수를 등록합니다.
          • element.removeEventListener(type, listener) : 해당 요소에서 등록된 함수를 제거합니다.
          • event.preventDefault() : 이벤트가 발생했을 때 기본 동작을 취소합니다.
          • event.stopPropagation() : 이벤트의 버블링을 방지하기 위해 이벤트 전파를 중지합니다.
          1. 기타
          • window.location.href : 현재 페이지의 URL을 가져옵니다.
          • window.alert(message) : 경고 메시지를 출력합니다.
          • window.confirm(message) : 확인 메시지를 출력하고 사용자의 답변에 따라 Boolean 값을 반환합니다.
        • module
          • import
          • export

필수 요구사항은 모두 만족했고 선택요구사항은 외부 API 사용과 flex, grid 사용을 제외하고 만족시켰다.

더보기
  • 선택요구사항
    • CSS
      • flex 사용하기
      • grid 사용하기
      • 반응형 UI 구성하기
    • 상세페이지 리뷰 수정 및 삭제 기능 구현
    • 메인 페이지
      • 조건에 맞는 카드 리스트 정렬 기능(이름순, 별점순 등 자유롭게)
    • 위에서 설명하지 않은 기타 외부 Api
    ✅ 원하는 추가 기능 무엇이라도 okay!
    • 여러분의 챌린지는 언제나 환영합니다. 필수 요구사항이 완료되었다면, 자유롭게 추가기능을 넣어주세요.
    • 단, 우선순위는 필수요구사항임을 명심해주세요

 

와이어프레임

miro라는 온라인 협업 공간에서 만들었다

 

 

내가 수행한 업무

 나는 본 프로젝트에서 팀장을 맡게 되었고 가장 먼저 역할을 나눴다.

리뷰 작성 페이지에서 localStorage를 사용하여 CRUD를 구현하는 부분을 맡게 되었다.

 

localStorage는 객체 형식으로 데이터를 저장하기 때문에 이를 어떻게 사용해야 정상적으로 작동할 지 고민했다.

 

[JavaScript] localStorage

localStorage는 웹 스토리지 객체(Web Storage Object)로 브라우저 내에 객체로서 저장할 수 있게끔 해주는 문법이다.localStorage에 저장한 값은 웹을 새로고침하거나 꺼도 유지되며 다시 실행하면 데이터

dh-0e.tistory.com

 

getReview(): 가장 먼저 실행되는 함수로 현재 페이지에 해당하는 영화에 작성된 리뷰들을 불러옴

function getReview() {
  // 리뷰 불러오기
  if (localStorage.length <= 2 || localStorage.getItem("IDs") === "") return; // 저장소에 리뷰가 있다면 길이가 7이상임, IDs가 비어있다면 return
  let IDs = localStorage.getItem("IDs").split(","); // IDs(리뷰들의 ID를 모아놓은 string) 가져와서 쉼표(,)기준으로 split -> 배열 반환
  for (let i = 0; i < IDs.length; i++) {
    // IDs에 들어있는 각각의 ID에 저장된 정보들을 get 해서 출력
    if (IDs[i] === "") continue; // ID가 "" 처럼 비어있는 것이 가끔 오류로 들어오면 null로 출력되므로 continue
    const ID = IDs[i];
    const movieID = localStorage.getItem(ID + "movieID");
    if (movieID !== localStorage.getItem("exportId")) continue; // ID가 저장되어있는 영화(movieID)가 현재 상세 페이지의 ID(exportID)와 같지 않다면 continue (현재 상세페이지에 저장된 리뷰들을 가져오기 위함)
    const name = localStorage.getItem(ID + "name");
    const msg = localStorage.getItem(ID + "msg");
    const time = localStorage.getItem(ID + "time");
    // const pw = localStorage.getItem(ID + "pw"); // pw는 표시하지 않아도 됨
    const data = {
      // 객체로 정보를 모아서
      ID: ID,
      name: name,
      msg: msg,
      time: time
    };
    showReview(data); // 화면에 출력해주는 함수에 보냄
  }
}

getReview(); //(1)

 

 

showReview(data): data 객체를 받아서 html에 추가하여 화면에 나타나게 해주는 함수

function showReview(data) {
  // data에 담긴 리뷰들을 화면에 출력
  let { ID, name, msg, time } = data; // 구조분해할당으로 받아온 데이터를 각각의 변수에 넣어주고
  let toChange = document.createElement("div");
  toChange.innerHTML = `
    <div class="comment row">
        <div class="col">
            <div class="author fw-bold">${name}</div>
            <div class="col-auto">
                <div class="time">${time}</div>
            </div>
            <div class="content mt-2">${msg}</div>
        </div>
        <div class="col-auto">
            <button class="btn btn-danger" id="${ID}" style="margin-right: -16px; margin-top: 9px;">수정 및 삭제</button>
        </div>
    </div>`;
  reviewSpace.appendChild(toChange); // 이렇게 html에 바로 넣지 않고 append child 하는 이유가 += 으로 넣으면 저장소가 달라져서 이전의 수정 및 삭제 버튼들의 이벤트가 전부 사라지게 됨
  buttonEditDelete = document.querySelectorAll(".btn.btn-danger"); // 수정 및 삭제 버튼을 모두 가져와서
  console.log(buttonEditDelete);
  makeEvent(buttonEditDelete[buttonEditDelete.length - 2]); // 이벤트를 만들어줌 (-2한 이유는 0부터 시작해서 -1, 맨 뒤에 모달의 삭제 버튼도 class가 같아서 들어옴 -1)
}

 

 

makeEvent(data): data 객체를 받아 '수정 및 삭제'라는 버튼 클릭시 이벤트를 걸어줌

function makeEvent(data) {
  // 수정 및 삭제에 이벤트를 만들어줌
  data.addEventListener("click", function (element) {
    // 수정 및 삭제를 누른다면
    const ID = element.target.id;
    modalName.value = localStorage.getItem(ID + "name"); // name input에 이름을 미리 넣어줌
    modalMessage.value = localStorage.getItem(ID + "msg"); // msg input에 내용을 미리 넣어줌
    modal.style.display = "block"; // 모달창 띄우기
    function deleteEventHandler() {
      // 삭제를 누를 시 delete로 editOrDelete함수 실행
      editOrDelete("delete", ID);
    }
    function editEventHandler() {
      // 수정을 누를 시 edit로 editOrDelete함수 실행
      if (editValidation()) {
        // validcheck.js에서 import해온 적합성 검사 함수를 실행하여 검사를 통과하면 함수 호출
        editOrDelete("edit", ID);
      }
    }
    function cancelEventHandler() {
      // 취소를 눌렀을 때 이벤트를 지워줘야함 or 다른 리뷰의 수정 및 삭제에서 이벤트가 살아있어 하나뿐인 모달에 두 개 이상의 이벤트가 들어가서 여러 개의 리뷰가 삭제될 수도 있음
      modal.style.display = "none"; // 모달창 꺼주고
      deleteButton.removeEventListener("click", deleteEventHandler); // 삭제 버튼의 이벤트를 삭제해줌
      editButton.removeEventListener("click", editEventHandler); // 수정 버튼의 이벤트를 삭제해줌
    }
    deleteButton.addEventListener("click", deleteEventHandler); // 이렇게 따로 이벤트를 만들어줌
    editButton.addEventListener("click", editEventHandler);
    cancelButton.addEventListener("click", cancelEventHandler);
  });
}

 

 

editOrDelete(kind, ID): 수정 버튼 or 삭제 버튼을 눌렀을 시 실행되는 함수. 우선 ID에 일치하는 정보들을 삭제한 후 kind가 'edit' 라면 수정한 정보들을 다시 입력해준다.

function editOrDelete(kind, ID) {
  // 수정 혹은 삭제
  if (modalPassword.value !== localStorage.getItem(ID + "pw")) {
    // 비밀번호 검사
    alert("비밀번호가 일치하지 않습니다!");
    return;
  }
  let editIDs = localStorage.getItem("IDs").split(","); // 마찬가지로 아이디들을 쉼표(,)기준으로 split하여 배열로 받아옴
  editIDs = editIDs.filter(function (val) {
    // IDs에 저장된 아이디들 중에 현재 ID를 미리 삭제해놓음 (빈칸 아이디가 있을 경우 이것도 삭제해줌)
    return val != ID && val != "";
  });
  localStorage.removeItem(ID + "name"); // 나머지 아이디의 정보들도 삭제해줌
  localStorage.removeItem(ID + "msg");
  localStorage.removeItem(ID + "pw");
  localStorage.removeItem(ID + "movieID");
  localStorage.removeItem(ID + "time");
  if (kind === "delete") {
    // 삭제라면 더이상 할 것이 없음
    alert("삭제가 완료되었습니다.");
  } else {
    // 수정이라면 수정된 내용을 포함해 정보들을 저장소에 추가해야함
    let IDs = editIDs,
      changeId = modalName.value; // changedId는 수정된 이름, IDs는 현재 존재하는 리뷰의 아이디들
    if (IDs.find((val) => val === changeId)) {
      // 겹치는 ID가 있다면
      let a = 0; // ID 뒤에 숫자를 붙여준다
      while (IDs.find((val) => val === changeId + a)) {
        // 숫자를 계속 증가시켜주면서 겹치는 게 없을 때까지 while 돌려준 뒤
        a++;
      }
      changeId += a; // 뒤에 숫자를 붙여줌
    }
    localStorage.setItem(changeId + "name", modalName.value); // 수정된 정보 저장
    localStorage.setItem(changeId + "msg", modalMessage.value);
    localStorage.setItem(changeId + "pw", modalPassword.value);
    localStorage.setItem(changeId + "movieID", localStorage.getItem("exportId")); // 현재 페이지의 영화 ID(exportID)를 저장
    const today = new Date().toLocaleString();
    localStorage.setItem(changeId + "time", today);
    editIDs.push(changeId); // 수정된 ID를 ID배열에 추가해주고
    alert("수정이 완료되었습니다.");
  }
  localStorage.setItem("IDs", editIDs); // IDs를 업데이트 해줌
  modal.style.display = "none"; // 모달창 비활성화
  location.reload(true); // 새로고침
}

 

 

inputClicked(): '입력' 버튼이 눌리면 실행되는 함수인데 이는 유효성 검사를 구현한 js에서 import하여 호출하도록 했다.

export function inputClicked() {
  // 입력 누를 시
  const name = getName.value; // 입력된 정보들 가져오기
  const pw = getPassword.value;
  const msg = getMessage.value;
  const movieID = localStorage.getItem("exportId"); // 영화 ID엔 현재 페이지의 ID(exportID) 저장
  const today = new Date().toLocaleString();
  let ID = name;
  if (
    localStorage.getItem("IDs") !== null &&
    localStorage.getItem("IDs") !== undefined &&
    localStorage.getItem("IDs") !== ""
  ) {
    // IDs에 중복된 ID가 있는지 확인하려는데 비어있거나 그런 경우를 제외시켜줌
    let IDs = localStorage.getItem("IDs").split(","); // IDs를 불러와서 쉼표(,)기준으로 나눠 배열로 만들어줌
    if (IDs.find((val) => val === ID)) {
      // 겹치는 ID가 있다면
      let a = 0; // 뒤에 숫자를 붙여 저장
      while (IDs.find((val) => val === ID + a)) {
        // 중복되는 숫자가 없을 때까지 ++
        a++;
      }
      ID += a; // ID뒤에 숫자를 붙여줌
    }
  }
  const data = {
    // 객체에 넣어 출력 함수에 보냄
    ID: ID,
    name: name,
    msg: msg,
    time: today
  };
  showReview(data); // 코드 추가

  let editIDs = localStorage.getItem("IDs"); // editIDs에 ID들 불러오기
  if (editIDs === null || editIDs === "") editIDs = [];
  else editIDs = [editIDs];
  editIDs.push(ID); // editIDs에 ID 넣기
  localStorage.setItem("IDs", editIDs); // ID 넣은 editIDs 저장
  localStorage.setItem(ID + "name", name); // 나머지 정보 저장
  localStorage.setItem(ID + "pw", pw);
  localStorage.setItem(ID + "msg", msg);
  localStorage.setItem(ID + "movieID", movieID);
  localStorage.setItem(ID + "time", today);

  getName.value = null;
  getPassword.value = null;
  getMessage.value = null;
}

 

 

displayMovies(data, howToSort): script.js에 있는 함수, 객체.sort()를 사용하여 정렬 기능을 추가함

function displayMovies(data, howToSort) {
  editId.innerHTML = null;
  let sortedData = (sortStd) => {
    if (sortStd === "sortTitle") {
      // 제목순
      return data.results.sort((a, b) => {
        if (a.title > b.title) return 1;
        else if (a.title < b.title) return -1;
        return 0;
      });
    } else if (sortStd === "sortRating") {
      // 평점순
      return data.results.sort((a, b) => {
        if (a.vote_average < b.vote_average) return 1;
        else if (a.vote_average > b.vote_average) return -1;
        return 0;
      });
    } else {
      // 개봉일순
      return data.results.sort((a, b) => {
        if (a.release_date < b.release_date) return 1;
        else if (a.release_date > b.release_date) return -1;
        return 0;
      });
    }
  };
  data = sortedData(howToSort); // 정렬된 배열 반환
  for (let movie of data) {
    editId.innerHTML += getMovieCode(movie);
  }
}

 

 

막혔던 부분

 

1. 모달 창을 하나로만 구현하여 코드에서 수정이나 삭제중 한 개의 버튼에 여러 개의 이벤트들이 걸리고 삭제되지 않은 경우 동시에 여러 개의 이벤트들이 실행되는 오류가 발생함.

removeEventLisner로 '취소' 버튼을 누르면 이벤트를 삭제시켜 해결함.

 

2. innerHTML에 더하기 할당 부호로 코드를 삽입할 시에 코드의 저장소가 달라져 걸려있던 이벤트가 실행이 되지 않는 것을 발견함.

appendChild로 코드 삽입 후 이벤트를 직접 하나씩 걸어주어 해결함.