카테고리 없음

데브매칭 (고양이 사진검색 사이트)

colazoa 2023. 3. 4. 03:08
반응형

이번문제는 완성되어있는코드를 고치는 과정이다 그래서 모든코드를 여기에 다쓰면 글이너무 길어질것같아서 수정하는 요소들만 코드를 쓰고 마지막 완성이미지만 올리겠다.

 

https://school.programmers.co.kr/skill_check_assignments?page=2

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

여기서 응시하고 들어가면 동일한 환경에서 코드를작성할수있다. 따로 복붙을 해서 로컬환경에서만들어도되지만 스타일이 미묘하게 다르고 이미지도 다 넣어줘야되기때문에 완성된 준비환경에서 하는걸 추천!

 

우선 들어가서 문제를 보면 

 

1.결과화면 반응형구현

  • 유저가 사용하는 디바이스의 가로 길이에 따라 검색결과의 row 당 column 갯수를 적절히 변경해주어야 합니다.
    • 992px 이하: 3개
    • 768px 이하: 2개
    • 576px 이하: 1개

검색한후 이미지

고양이를 검색하면 이렇게 이미지가 나오는데 이 이미지들을 반응형으로 작성해서 

screen 사이즈에따라 보이는 열의 갯수를 다르게 해줘야하는 요구사항이다.

css코드를 보면

.SearchResult {
  margin-top: 10px;
  display: grid;
  grid-template-columns: repeat(4, minmax(250px, 1fr));
  grid-gap: 10px;
}

결과를 보여주는 창의 스타일이 그리드로 잡혀있는걸 볼수있다 이제 이 아래에 반응형 스타일을 추가해주면 된다.

@media screen and (max-width: 992px) {
  .SearchResult {
    grid-template-columns: repeat(3,  1fr);
  }
}
@media screen and (max-width: 768px) {
  .SearchResult {
    grid-template-columns: repeat(2,  1fr);
  }
}
@media screen and (max-width: 576px) {
  .SearchResult {
    grid-template-columns: repeat(1,  1fr);
  }
}

이제 화면크기 992,768,576 이하일때 스타일이 각각 다르게 적용된다 

 

2.다크모드 구현

  • 다크 모드(Dark mode)를 지원하도록 CSS를 수정해야 합니다.
    • CSS 파일 내의 다크 모드 관련 주석을 제거한 뒤 구현합니다.
    • 모든 글자 색상은 #FFFFFF , 배경 색상은 #000000 로 한정합니다.
    • 기본적으로는 OS의 다크모드의 활성화 여부를 기반으로 동작하게 하되, 유저가 테마를 토글링 할 수 있도록 좌측 상단에 해당 기능을 토글하는 체크박스를 만듭니다.

배경색과 글자의 색을 변수로 지정해준다.

:root {
  --main-bg-color: #ffffff;
  --main-color: #000000;
}
// html전체에 적용
html {
  box-sizing: border-box;
  background-color: var(--main-bg-color);
  color: var(--main-color);
}

컴퓨터의 다크모드가 활성화되있는지를 확인해서 변수값을 변경시켜준다

@media (prefers-color-scheme: dark) {
  :root {
    --main-bg-color: #000000;
    --main-color: #ffffff;
  }
}

다크모드가 활성화되어있으면 글자색과 배경색 반전

 

이제 유저가 다크모드로 전환할수있는 토글버튼을 만들어준다. 컴포넌트로 만들어서 app에서 추가해준다.

src/ToggleDark.js

class ToggleDark {
    constructor({target}) {
      console.log(target);
      const $toggleButton = document.createElement("input");
      $toggleButton.type = "checkbox";
      $toggleButton.id = "toggle";
      const $togglelabel = document.createElement("Label");
      $togglelabel.setAttribute("for", $toggleButton.id);
      $togglelabel.innerHTML = "다크모드";
      target.appendChild($toggleButton);
      target.appendChild($togglelabel);
      
      const prefersColorScheme = window.matchMedia("(prefers-color-scheme: dark)");
      $toggleButton.checked=prefersColorScheme.matches
  
      // 체크박스에 이벤트 리스너 생성
      $toggleButton.addEventListener("click", e => {
      if ($toggleButton.checked) {
          document.documentElement.style.setProperty(`--main-bg-color`, "#000000");
          document.documentElement.style.setProperty(`--main-color`, "#ffffff");
      } else {
          document.documentElement.style.setProperty(`--main-bg-color`, "#ffffff");
          document.documentElement.style.setProperty(`--main-color`, "#000000");
      }
      });
    }
}

이미작성되어있는 컴포넌트들이 class 형으로 작성되어있어서 ToggleDark도 클래스 형으로 작성했다.

렌더링될위치인 target을 전달받는다 . 

document.createElement 로 input태그를 생성하고 타입을 checkbox로 지정한다. id로 toggle을 넣어준다

document.createElement 로 label태그를 생성하고 속성을 for로 지정한후 toggleButton.id 로 동일한 id값을 줘서 라벨을 연결해준다. innerHTML 로 label태그에 "다크모드" 텍스트를 넣어준다

appendChild로 새로만든 태그 2개다 target에 자식요소로 연결해준다

prefersColorScheme 는 컴퓨터의 다크모드여부를 받아온다 다크모드이면 matches에 true가 담기고 아니면 false가 담긴다.

toggleButton에 클릭이벤트를 생성하고 체크하면 다크모드일때의 root 값으로 false이면 아닐때의 root값을 적용

 

이제 만든 ToggleDark.js 컴포넌트를 연결해줘야한다 

 

index.html에서 불러와주고

    <div id="App"></div>
    <script src="src/utils/validator.js"></script>
    <script src="src/api.js"></script>
    <script src="src/ImageInfo.js"></script>
    <script src="src/SearchInput.js"></script>
    <script src="src/SearchResult.js"></script>
    <script src='src/ToggleDark.js'></script>  <--여기 main.js보다는 윗줄에있어야함
    <script src="src/App.js"></script>
    <script src="src/main.js"></script>

app.js에서 렌더링 해줘야한다.

  constructor($target) {
    this.$target = $target;

    this.toggledark = new ToggleDark({
      target:$target
    })
    this.searchInput = new SearchInput({
      $target,
      onSearch: keyword => {
        api.fetchCats(keyword).then(({ data }) => this.setState(data));
      }
    });

    this.searchResult = new SearchResult({
      $target,
      initialData: this.data,
      onClick: image => {
        this.imageInfo.setState({
          visible: true,
          image
        });
      }
    });

전달받는함수나 전달하는 함수가없어서 순서는 중요하지않지만 constructor안에 있어야 클래스가 만들어질때 실행시킬수있다.

 

3.이미지 모달 닫기/ 사이즈 반응형

  • 디바이스 가로 길이가 768px 이하인 경우, 모달의 가로 길이를 디바이스 가로 길이만큼 늘려야 합니다.
  • 필수 이미지를 검색한 후 결과로 주어진 이미지를 클릭하면 모달이 뜨는데, 모달 영역 밖을 누르거나 / 키보드의 ESC 키를 누르거나 / 모달 우측의 닫기(x) 버튼을 누르면 닫히도록 수정해야 합니다.
  • 모달에서 고양이의 성격, 태생 정보를 렌더링합니다. 해당 정보는 /cats/:id 를 통해 불러와야 합니다.
  • 추가 모달 열고 닫기에 fade in/out을 적용해 주세요.

모달의 가로길이부터 반응형으로 설정해주겠다.

@media screen and (max-width: 768px) {
  .SearchResult {
    grid-template-columns: repeat(2,  1fr);
  }
  .ImageInfo .content-wrapper {
    width: 100%;
  }
}

아까 만든 반응형에 ImageInfo .content-wrapper 를 넣어서 768이하일때 너비가 100%되도록 해줬다.

 

모달을 열고닫을 때  fadein fadeout 될수있게 애니메이션을 만들어준다.

@keyframes fadeIn {
  from { opacity: 0;}
  to { opacity: 1;}
}
@keyframes fadeOut {
  from { opacity: 1;}
  to { opacity: 0;}
}

ImageInfo.js를 수정해준다

 

src/ ImageInfo.js

class ImageInfo {
  $imageInfo = null;
  data = null;

  constructor({ $target, data }) {
    const $imageInfo = document.createElement("div");
    $imageInfo.className = "ImageInfo";
    this.$imageInfo = $imageInfo;
    $target.appendChild($imageInfo);

    this.data = data;
    /*
    click 이벤트 추가
    클릭했을때 클릭한타겟을 클래스를받아서 클래스이름이 close 이거나 ImageInfo이면 
    this.fadeout() fade아웃되면서 display:none
    */ 
    this.$imageInfo.addEventListener('click',e=>{
      let targetName = e.target.className;
      if(targetName==='close' || targetName === 'ImageInfo'){
        this.fadeOut();
      }
    })
    //esc키를 눌렀을때도 모달창이 닫히게 "keydown"이벤트추가
    window.addEventListener('keydown',e=>{
      if(e.key==="Escape"){
        this.fadeOut();
      }
    })

    this.render();
  }

  setState(nextData) {
    this.data = nextData;
    this.render();
  }

  render() {
    if (this.data.visible) {
      const { name, url, temperament, origin } = this.data.image;

      this.$imageInfo.innerHTML = `
        <div class="content-wrapper">
          <div class="title">
            <span>${name}</span>
            <div class="close">x</div>
          </div>
          <img src="${url}" alt="${name}"/>        
          <div class="description">
            <div>성격: ${temperament}</div>
            <div>태생: ${origin}</div>
          </div>
        </div>`;
      // this.$imageInfo.style.display = "block";
      this.fadeIn()
    } else {
      // this.$imageInfo.style.display = "none";
      this.fadeOut()
    }
  }
  //fadeout메서드 fadeout애니메이션을 실행시키고 0.5초뒤에 display:none;
  fadeOut() {
    this.$imageInfo.style.animation = "fadeOut 0.5s";
    this.$imageInfo.style.tanimationFillMode = "forwards";
    setTimeout(()=>{
      this.$imageInfo.style.display = "none";
    },500) 
  }
  //fadein메서드 display:block으로 보이게해주고 fadeIn 애니메이션 실행
 fadeIn() {
    this.$imageInfo.style.display = "block";
    this.$imageInfo.style.animation = "fadeIn 0.5s";
    this.$imageInfo.style.tanimationFillMode = "forwards";
  }
}

구문이 길다보니 주석으로 중요한부분을 적어줬다.. fadeIn,fadeout 메서드를 만들고 

constructor안에 클릭이벤트와 키다운이벤트를 만들어줬다. 

원래 this.$imageInfo.style.display = "block" 이런식으로 보이게만들어주고 안보이게만들어주는 방식이었는데 여기를 모두 fadeIn fadeOut 메서드를 호출하는방식으로 바꿔주었다

 

5. 검색페이지

검색 페이지 관련

  • 페이지 진입 시 포커스가 input 에 가도록 처리하고, 키워드를 입력한 상태에서 input 을 클릭할 시에는 기존에 입력되어 있던 키워드가 삭제되도록 만들어야 합니다.
  • 필수 데이터를 불러오는 중일 때, 현재 데이터를 불러오는 중임을 유저에게 알리는 UI를 추가해야 합니다.(loading)
  • 필수 검색 결과가 없는 경우, 유저가 불편함을 느끼지 않도록 UI적인 적절한 처리가 필요합니다.
  • 최근 검색한 키워드를 SearchInput 아래에 표시되도록 만들고, 해당 영역에 표시된 특정 키워드를 누르면 그 키워드로 검색이 일어나도록 만듭니다. 단, 가장 최근에 검색한 5개의 키워드만 노출되도록 합니다.
  • 페이지를 새로고침해도 마지막 검색 결과 화면이 유지되도록 처리합니다.
  • 필수 SearchInput 옆에 버튼을 하나 배치하고, 이 버튼을 클릭할 시 /api/cats/random50 을 호출하여 화면에 뿌리는 기능을 추가합니다. 버튼의 이름은 마음대로 정합니다.
  • lazy load 개념을 이용하여, 이미지가 화면에 보여야 할 시점에 load 되도록 처리해야 합니다.

포커스부터 해결해보자..

 

SearchInput.js 수정

    this.$searchInput.focus();
    $searchInput.addEventListener("click", e=>{
        $searchInput.value = "";
    })

constructor안에 이 구문을 추가해준다. input태그에 focus를 주고 인풋태그를 클릭했을때 값이 지워지게 해준다.

 

이제 로딩창을 만들어보자.

Loading.js 생성

//렌더링해줄 html요소
const template = `
    <div class="loading"></div>
    <div id="loading-text">loading</div>
`;

class LoadingInfo {
    $loadingInfo = null;
    data = null;
  
    constructor({ $target, data }) {
        const $loadingInfo = document.createElement("div"); //div태그생성
        $loadingInfo.className = "loading-container"; //class달아주기
        this.$loadingInfo = $loadingInfo; //constructor밖에 있는 loadingInfo에 할당
        this.data = data; //파라미터로 받아온 data 할당
        //data.visible이 true면 보이고 false면 안보이게 설정
        this.$loadingInfo.style.display = this.data.visible ? 'block' : 'none';
        $target.appendChild($loadingInfo); //타겟에 자식요소로 연결
        this.render(); //렌더 메소드 실행 html요소 그려줌
    }
	//onChange메소드
    //실행되면 visible을 토글해주고 display스타일을 다시 불러줌
    //보이고있었으면 안보이게해주고 ,안보이고있었으면 보이게 바꿔준다.
    onChange() {
        this.data.visible = !this.data.visible;
        this.$loadingInfo.style.display = this.data.visible ? 'block' : 'none';
    }
  	//render메소드 loadingInfo에 innerHTML로 template요소 넣어줌
    render() {
        this.$loadingInfo.innerHTML = template;
    }
}

이것도 아까 ToggleDark 처럼 index.html 에서 script 연결해주고 

App.js에서 constructor안에 불러서 사용하면 된다 자세한 설명은 생략한다.. 

 

api수정 키워드와 id random으로 각각 출력할수있는 함수생성

api.js

// const API_ENDPOINT =
//   "https://q9d70f82kd.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()
//     );
//   }
// };
//원래 작성되어있던코드 keyword를 받아서 고양이 사진들을 받아왔었다.

//api주소
const API_ENDPOINT =
  "https://q9d70f82kd.execute-api.ap-northeast-2.amazonaws.com/dev";

const api = {
	//키워드를 받아서 api에서 키워드에 해당하는 데이터를 받아옴
  fetchCats: async keyword => {
    try{
      const response = await fetch(`${API_ENDPOINT}/api/cats/search?q=${keyword}`);
      const data = await response.json();
      return data;
    }
    catch(e){
      console.log(e);
    }
  },
  //id를 받아서 해당이미지의 데이터를 받아옴
  fetchCat: async id => {
    try {
      const response = await fetch(`${API_ENDPOINT}/api/cats/${id}`);
      const data = await response.json();
      console.log(data);
      return data;
    }
    catch(e) {
      console.log(e);
    }
  },
  //랜덤하게 데이터를 받아옴
  fetchRandom : async () => {
    try {
      const response = await fetch(`${API_ENDPOINT}/api/cats/random50`);
      const data = await response.json();
      return data;
    }
    catch(e) {
      console.log(e);
    }
  }
};

불러오는 함수를 바꿨으니 app.js에서 쓰던 함수도 변경해줘야한다.

app.js 수정

    this.searchInput = new SearchInput({
      $target,
      // onSearch: keyword => {
      //   api.fetchCats(keyword).then(({ data }) => this.setState(data));
      // }
      onSearch: async keyword => {
        try{
          this.loadingInfo.onChange();
          const {data} = await api.fetchCats(keyword);
          this.setState(data);
        }catch(e){
          console.error(e);
        }finally{
          this.loadingInfo.onChange();
        }
      }
    });

async 키워드로 비동기로 데이터를 받아오고 실행될때 loadingInfo.onChange() 를 실행시켜서 로딩창이 보이게되고

데이터를 다받아오거나 실패했을때 finally에서 다시 onChange()를 실행해줘서 로딩창이보이지않게된다.

    this.searchResult = new SearchResult({
      $target,
      initialData: this.data,
      // onClick: image => {
      //   this.imageInfo.setState({
      //     visible: true,
      //     image
      //   });
      // }
      onClick: image => {
        api.fetchCat(image.id).then(({data}) => 
        this.imageInfo.setState({
          visible: true,
          image:data
        })
        )
      }
    });

원래는 image에 값이 없어서 고양이이미지에서 이름이나 자료가 나오지않았는데 아이디로 다시 data를 받아와서 상태값을 업데이트해서 성격,태생 자료가 나오게된다

위쪽이 id로 fetch한 data이고

아래는 클릭한 이미지의 data이다 id,name,url밖에 들어있지않다.

반응형