💡 Breadcrumb
브레드크럼이란 헨젤과 그레텔에서 따온 용어로, 사이트나 웹 앱에서 유저의 위치를 보여주는 부차적인 내비게이션을 뜻한다.
전체 구조 안에서 유저가 어디에 있는지 알려주기 용이하며 전체 구조 이해에 도움을 준다.
API
❗API가 상당히 불안정하여 어떤 때에는 작동하고 어떤 때에는 작동하지 않는다.
https://cat-photos.edu-api.programmers.co.kr
[
{
"id":"1",
"name":"노란고양이",
"type":"DIRECTORY",
"filePath":null,
"parent":null
},
{
"id":"3",
"name":"까만고양이",
"type":"DIRECTORY",
"filePath":null,
"parent":null
}
]
https://cat-photos.edu-api.programmers.co.kr/${id}
[
{
"id":"5",
"name":"2021/04",
"type":"DIRECTORY",
"filePath":null,
"parent":{"id":"1"}
},
{
"id":"19",
"name":"물 마시는 사진",
"type":"FILE",
"filePath":"/images/a2i.jpg",
"parent":{"id":"1"}
}
]
CSS
https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/css/cat-photos.css
고양이 이미지
https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/${node.filePath}
예시 : https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/a2i.jpg
파일, 디렉토리, 뒤로가기 이미지 주소
https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/file.png
https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/directory.png
https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/prev.png
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>고양이 사진첩</title>
<link rel="stylesheet" href="https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/css/cat-photos.css">
</head>
<body>
<main class="App"></main>
<script src="/src/main.js" type="module"></script>
</body>
</html>
import App from './App.js'
const $target = document.querySelector('.App')
new App({$target})
const API_END_POINT = "https://cat-photos.edu-api.programmers.co.kr/"
export const request = async(url) => {
try{
const res = await fetch(`${API_END_POINT}${url}`)
if(!res.ok){
throw new Error ('api 호출 오류')
}
return await res.json()
} catch(e){
alert(e.message)
}
}
export default function Nodes({ $target, initialState, onClick, onPreClick }){
/*
initialState: {
isRoot : false,
nodes : []
},
*/
const $nodes = document.createElement('div')
$nodes.classList.add('Nodes')
$target.appendChild($nodes)
this.state = initialState
this.setState = (nextState) => {
this.state = nextState
this.render()
}
this.render = () => {
const {isRoot, nodes} = this.state
$nodes.innerHTML = `
${isRoot ? '' : `
<div class="Node">
<img src="https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/prev.png"/>
</div>`
}
${nodes.map(node => `
<div class="Node" data-id="${node.id}">
<img src="${node.type === 'DIRECTORY' ?
"https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/directory.png" : "https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/file.png" }" >
${node.name}
</div>
`).join(' ')}
`}
this.render()
$nodes.addEventListener('click', (e) => {
const $node = e.target.closest('.Node')
const {id} = $node.dataset
// id가 없는 경우는 뒤로가기를 누른 경우이다.
if(!id){
// 뒤로가기 처리
}
const node = this.state.nodes.find(node => node.id === id)
if (node){
onClick(node)
} else {
onPreClick()
}
})
}
export default function ImageViewer({$target,onClose}){
const $imageViewer = document.createElement('div')
$imageViewer.className = 'ImageViewer Modal'
$target.appendChild($imageViewer)
this.state = {
selectedImageUrl : null
}
this.setState = (nextState) =>{
this.state = nextState
this.render()
}
this.render = () => {
$imageViewer.style.display = this.state.selectedImageUrl? 'block' : 'none'
if(this.state.selectedImageUrl){
$imageViewer.innerHTML = `
<div class='content'>
<image src='${this.state.selectedImageUrl}'/>
</div>`
}
}
this.render()
// esc 키 누르면 모달이 닫히는 처리
window.addEventListener('keyup', e => {
if(e.key === 'Escape'){
onClose()
}
})
// 모달창 바깥을 클릭하면 모달창 닫히는 처리
$imageViewer.addEventListener('click', e => {
// includes는 배열에 쓸 수 있으므로 Array.from을 이용해 배열로 감쌈
if(Array.from(e.target.classList).includes('Modal')){
onClose()
}
})
}
export default function Loading({$target}){
const $loading = document.createElement('div')
$loading.className = 'Loading'
$target.appendChild($loading)
this.state = false
this.setState = nextState => {
this.state = nextState
this.render()
}
this.render = () => {
$loading.innerHTML = `
<div class = 'Modal' style="text-align:center">
<img src="https://media.giphy.com/media/gx54W1mSpeYMg/giphy.gif" alt="loading...">
</div>
`
$loading.style.display = this.state? 'block' : 'none'
}
this.render()
}
export default function BreadCrumb({$target, initialState, onClick}){
const $breadCrumb = document.createElement('nav')
$breadCrumb.className = 'Breadcrumb'
$target.appendChild($breadCrumb)
this.state = initialState
this.setState = nextState => {
this.state = nextState
this.render()
}
this.render = () => {
$breadCrumb.innerHTML = `
<div class = "breadcrumb_item"> root <div>
${this.state.map(({name, id})=>
`<div class = "breadcrumb_item" data-id="${id}">- ${name}</div>`
).join(' ')}
`
}
this.render()
$breadCrumb.addEventListener('click', (e)=> {
const breadcrumbItem = e.target.closest('.breadcrumb_item')
const {id} = breadcrumbItem.dataset
onClick(id)
})
}
import Nodes from './Nodes.js'
import { request } from './Api.js'
import ImageViewer from './imageViewer.js'
import Loading from './Loading.js'
import BreadCrumb from './breadCrumb.js'
export default function App({$target}){
this.state = {
isRoot: true,
nodes: [],
paths: [],
isLoading: false
}
const loading = new Loading({$target})
const breadCrumb = new BreadCrumb({
$target,
initialState: this.state.paths,
onClick: async(id) => {
// 클릭한 경로 외에 path를 날려준다.
if(id) {
const nextPaths = [...this.state.paths]
const pathIndex = nextPaths.findIndex(path => path.id === id)
this.setState({
...this.state,
paths: nextPaths.slice(0, pathIndex + 1)
})
} else {
this.setState({
...this.state,
paths: []
})
}
// 클릭한 경로로 이동
await fetchNodes(id)
}
})
const nodes = new Nodes({
$target,
initialState: {
isRoot : this.state.isRoot,
nodes : this.state.nodes,
selectedImageUrl: null
},
onClick: async(node) => {
if(node.type === 'DIRECTORY'){
await fetchNodes(node.id)
this.setState({
...this.state,
paths: [...this.state.paths, node]
})
}
if(node.type === 'FILE'){
this.setState({
...this.state,
selectedImageUrl: `https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public${node.filePath}`
})
}
},
onPreClick: async() => {
const nextPaths = [...this.state.paths]
nextPaths.pop()
this.setState({
...this.state,
paths: nextPaths
})
if (nextPaths.length === 0 ){
await fetchNodes()
} else{
await fetchNodes(nextPaths[nextPaths.length-1].id)
}
}
})
const imageViewer = new ImageViewer({
$target,
onClose: () => {
this.setState({
...this.state,
selectedImageUrl: null
})
}
})
this.setState = (nextState) =>{
this.state = nextState
nodes.setState({
isRoot : this.state.isRoot,
nodes : this.state.nodes,
})
imageViewer.setState({
...this.state,
selectedImageUrl : this.state.selectedImageUrl
})
loading.setState(this.state.isLoading)
breadCrumb.setState(this.state.paths)
}
const fetchNodes = async(id) => {
this.setState({
...this.state,
isLoading: true
})
const nodes = await request(id? `${id}` : '')
this.setState({
...this.state,
nodes,
isRoot: id? false : true,
isLoading: false
})
}
fetchNodes()
}
⭐ 요구사항을 보고 어떤 컴포넌트를 만들고 조합할지 정리한 후 코드를 작성하자!!
CSS 문제로 breadCrumb가 root 옆으로 붙지 않는 것을 제외하고는 정상적으로 작동한다. 다만 API가 매우 불안정하여 로딩이 오래걸리며 fetch에 실패하는 경우가 매우 많다. (502 에러)