데브매칭 (고양이 사진검색 사이트)
이번문제는 완성되어있는코드를 고치는 과정이다 그래서 모든코드를 여기에 다쓰면 글이너무 길어질것같아서 수정하는 요소들만 코드를 쓰고 마지막 완성이미지만 올리겠다.
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밖에 들어있지않다.