[VanillaJS] 고양이 검색 사이트 🐈⬛ - 구현편 | protect-me
1. HTML, CSS 관련
1-1.
현재 HTML 코드가 전체적으로
<div>로만 이루어져 있습니다. 이 마크업을 시맨틱한 방법으로 변경해야 합니다.
header, nav, section, article, footer
1-2.
유저가 사용하는 디바이스의 가로 길이에 따라 검색결과의 row 당 column 갯수를 적절히 변경해주어야 합니다.
- 992px 이하: 3개, 768px 이하: 2개, 576px 이하: 1개
.SearchResult {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(4, minmax(250px, 1fr));
  grid-gap: 10px;
}
/* 992px 이하 3개*/
@media (max-width : 992px){
  .SearchResult{
      grid-template-columns: repeat(3, minmax(250px, 1fr));
  }
}
/* 768px 이하 적용 2개 */
@media (max-width : 768px){
  .SearchResult{
      grid-template-columns: repeat(2, minmax(250px, 1fr));
  }
}
/* 576px 이하 적용 1개 */
@media (max-width : 576px){
  .SearchResult{
      grid-template-columns: repeat(1, minmax(250px, 1fr));
  }
}1-3.
다크 모드(Dark mode)를 지원하도록 CSS를 수정해야 합니다.
CSS 파일 내의 다크 모드 관련 주석을 제거한 뒤 구현합니다.
모든 글자 색상은 #FFFFFF , 배경 색상은 #000000 로 한정합니다.
기본적으로는 OS의 다크모드의 활성화 여부를 기반으로 동작하게 하되, 유저가 테마를 토글링 할 수 있도록 좌측 상단에 해당 기능을 토글하는 체크박스를 만듭니다.
CSS 미디어 쿼리 prefers-color-scheme (다크 모드)
웹페이지 다크 모드 지원하기
App.js
import SearchInput from "./SearchInput.js";
import SearchResult from "./SearchResult.js";
import ImageInfo from "./ImageInfo.js";
import DarkmodeToggle from "./DarkmodeToggle.js";
import api from "./api.js";
console.log("app is running!");
export default class App {
  $target = null;
  data = [];
  constructor($target) {
    this.$target = $target;
    this.darkmodeToggle = new DarkmodeToggle($target)
    
    //...DarkmodeToggle.js
export default class DarkmodeToggle {
  constructor($target) {
    const darkModeWrapper = document.createElement("div");
    darkModeWrapper.className = "dark-mode-wrapper";
    this.darkModeWrapper = darkModeWrapper;
    this.currentMode = localStorage.getItem('theme')
      || (
        window.matchMedia("(prefers-color-scheme: dark)").matches
          ? "dark"
          : "light"
      )
    document.documentElement.setAttribute('data-theme', this.currentMode);
    $target.appendChild(darkModeWrapper);
    this.render();
  }
  toggleMode() {
    this.currentMode = this.currentMode === "dark" ? "light" : "dark"
    const darkModeBtn = document.querySelector('.dark-mode-btn')
    darkModeBtn.innerText = this.currentMode == "dark" ? "🌕" : "🌑"
    document.documentElement.setAttribute('data-theme', this.currentMode);
    localStorage.setItem('theme', this.currentMode);
  }
  render() {
    const darkModeBtn = document.createElement("span");
    darkModeBtn.className = "dark-mode-btn";
    darkModeBtn.innerText = this.currentMode == "dark" ? "🌕" : "🌑"
    darkModeBtn.addEventListener("click", this.toggleMode)
    this.darkModeWrapper.appendChild(darkModeBtn);
  }
}style.css
:root {
  --background: #fff;
  --text: #000;
}
@media (prefers-color-scheme: dark) {
  :root {
    --background: #000;
    --text: #fff;
  }
}
[data-theme="light"] {
  --background: #fff;
  --text: #000;
}
[data-theme="dark"] {
  --background: #000;
  --text: #fff;
}
body {
  background-color: var(--background);
  color: var(--text);
  transition: background 500ms ease-in-out, color 200ms ease;
}
.dark-mode-btn {
  font-size: 30px;
}2. 이미지 상세 보기 모달 관련
2-1.
디바이스 가로 길이가 768px 이하인 경우, 모달의 가로 길이를 디바이스 가로 길이만큼 늘려야 합니다.
style.css
@media (max-width : 576px){
  .SearchResult{
      grid-template-columns: repeat(1, minmax(250px, 1fr));
  }
  .ImageInfo .content-wrapper {
    width: 100vw;
  }
}2-2.
필수이미지를 검색한 후 결과로 주어진 이미지를 클릭하면 모달이 뜨는데, 모달 영역 밖을 누르거나 / 키보드의 ESC 키를 누르거나 / 모달 우측의 닫기(x) 버튼을 누르면 닫히도록 수정해야 합니다.
ImageInfo.js
export default class ImageInfo {
  $imageInfo = null;
  data = null;
  constructor({ $target, data }) {
    const $imageInfo = document.createElement("div");
    $imageInfo.className = "ImageInfo";
    this.$imageInfo = $imageInfo;
    $target.appendChild($imageInfo);
    document.addEventListener("keydown", (e) => { // ESC 버튼 클릭 시 닫기
      e.key === "Escape" ? this.closeImageInfo() : ''
    });
    this.data = data;
    this.render();
  }
  setState(nextData) {
    this.data = nextData;
    this.render();
  }
  closeImageInfo() {
    this.data.visible = false
    this.$imageInfo.style.display = "none";
    const $imageInfo = document.querySelector('.ImageInfo')
    $imageInfo.innerHTML = null
  }
  render() {
    if (this.data.visible) {
      const { name, url, temperament, origin } = this.data.image;
      const contentWrapper = document.createElement('div')
      contentWrapper.className = 'content-wrapper'
      const title = document.createElement('div')
      title.className = 'title'
      const titleText = document.createElement('span')
      titleText.innerText = name
      const closeButton = document.createElement('div')
      closeButton.className = 'close'
      closeButton.innerText = '❌'
      const image = document.createElement('img')
      image.setAttribute('src', url)
      image.setAttribute('alt', name)
      const description = document.createElement('div')
      const temperamentText = document.createElement('div')
      temperamentText.innerText = temperament
      const originText = document.createElement('div')
      originText.innerText = origin
      title.append(titleText, closeButton)
      description.append(temperamentText, originText)
      contentWrapper.append(title, image, description)
      this.$imageInfo.append(contentWrapper)
      this.$imageInfo.addEventListener('click', (event) => {
        if (event.target.className === 'ImageInfo'
          || event.target.className === 'close') {
          this.closeImageInfo()
        }
      })
      this.$imageInfo.style.display = "block";
    } else {
      this.$imageInfo.style.display = "none";
    }
  }
}2-3.
모달에서 고양이의 성격, 태생 정보를 렌더링합니다. 해당 정보는 /cats/:id 를 통해 불러와야 합니다.
api.js
import 'regenerator-runtime/runtime'
const API_ENDPOINT =
  "https://oivhcpn8r9.execute-api.ap-northeast-2.amazonaws.com/dev";
// const api = {
//   fetchCats: keyword => {
//     return fetch(`${API_ENDPOINT}/api/cats/search?q=${keyword}`).then(res =>
//       res.json()
//     );
//   },
// };
const request = async (url) => {
  try {
    const res = await fetch(url);
    if (res.ok) {
      const data = await res.json();
      return data;
    } else {
      const errData = await res.json();
      throw errData;
    }
  } catch (e) {
    throw {
      message: e.message,
      status: e.status,
    };
  }
};
const api = {
  fetchCats: async (keyword) => {
    try {
      const result = await request(
        `${API_ENDPOINT}/api/cats/search?q=${keyword}`
      );
      return result
    } catch (e) {
      return e;
    }
  },
  fetchDetailInfo: async (id) => {
    try {
      const result = await request(`${API_ENDPOINT}/api/cats/${id}`);
      return result
    } catch (e) {
      return e;
    }
  },
}
export default api;ImageInfo.js
import api from "./api.js";
(...)
  async render() {
    if (this.data.visible) {
      const { name, url, temperament, origin } = this.data.image;
      const detailInfo = await api.fetchDetailInfo(this.data.image.id);
      console.log('here', detailInfo);
      const contentWrapper = document.createElement('div')
      contentWrapper.className = 'content-wrapper'
      const title = document.createElement('div')
      title.className = 'title'
      const titleText = document.createElement('span')
      titleText.innerText = name
      const closeButton = document.createElement('div')
      closeButton.className = 'close'
      closeButton.innerText = '❌'
      const image = document.createElement('img')
      image.setAttribute('src', url)
      image.setAttribute('alt', name)
      const description = document.createElement('div')
      const temperamentText = document.createElement('div')
      temperamentText.innerText = `성격 : ${detailInfo.data.temperament}`
      const originText = document.createElement('div')
      originText.innerText = `태생 : ${detailInfo.data.origin}`
      (...)
2-4.
추가모달 열고 닫기에 fade in/out을 적용해 주세요.
ImageInfo.js
  (...)
 
  closeImageInfo() {
    this.$imageInfo.removeAttribute('data-status')
    this.$imageInfo.setAttribute('data-status', 'hidden');
    setTimeout(() => {
      this.$imageInfo.removeAttribute('data-status');
      this.$imageInfo.setAttribute('data-status', 'removed');
      this.$imageInfo.innerHTML = null
      this.data.visible = false
      this.$imageInfo.style.display = "none";
    }, 1000);
  }
  async render() {
    console.log('render');
    if (this.data.visible) {
      this.$imageInfo.setAttribute('data-status', 'shown')
  
  (...)
style.css
.ImageInfo {
  position: fixed;
  left: 0;
  top: 0;
  width: 100vw;
  height: 100vh;
  background-color: rgba(0, 0, 0, 0.5);
}
/* REF: https://stackoverflow.com/questions/19466670/trigger-css-animation-fade-in-fade-out-with-javascript-only-works-in-one-dir */
@keyframes fade {
  0% { opacity: 0; }
  100% { opacity: 1; display: block; }
}
.ImageInfo[data-status="shown"] {
  opacity: 1;
  animation: fade 1s;
}
.ImageInfo[data-status="hidden"] {
  opacity: 0;
  animation: fade 2s;
  animation-direction: reverse;
}
.ImageInfo[data-status="removed"] {
  display: none;
}3. 검색 페이지 관련
4. 스크롤 페이징 구현
5. 랜덤 고양이 배너 섹션 추가
6. 코드 구조 관련
Photo by Hannah Troupe on Unsplash
Author And Source
이 문제에 관하여([VanillaJS] 고양이 검색 사이트 🐈⬛ - 구현편 | protect-me), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@protect-me/VanillaJS-고양이-검색-사이트-구현편-protect-me저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
                                
                                
                                
                                
                                
                                우수한 개발자 콘텐츠 발견에 전념
                                (Collection and Share based on the CC Protocol.)