1. 고양이 사진 API를 통해 사진과 폴더를 렌더링한다.
2. 폴더를 클릭하면 내부 폴더의 사진과 폴더를 보여준다.
현재 경로가 어디인지도 렌더링한다.
3. 루트 경로가 아닌 경우, 파일 목록 맨 앞에 뒤로가기를 넣는다.
4. 사진을 클릭하면 고양이 사진을 모달창으로 보여준다.
esc를 누르거나 사진 밖을 클릭하면 모달을 닫는다.
5. API를 불러오는 중인 경우 로딩 중임을 알리는 처리를 한다.
🔹 컴포넌트 구성
💡 Breadcrumb
브레드크럼이란 헨젤과 그레텔에서 따온 용어로, 사이트나 웹 앱에서 유저의 위치를 보여주는 부차적인 내비게이션을 뜻한다.
전체 구조 안에서 유저가 어디에 있는지 알려주기 용이하며 전체 구조 이해에 도움을 준다.
❗ 이 API는 AWS를 이용해 제작되었다. 문제는 API가 상당히 불안정하여 어떤 때에는 작동하고 어떤 때에는 작동하지 않는다. (...) CORS Error 해결을 위해 확장프로그램까지 설치했지만 큰 진전은 없었다. 제작자님... 제발...
❗ state 정합성 체크가 아직 들어가지 않은 부분이 있다. 오류로 멈춰서는 부분을 찾아 수정이 진행중이다.
❗ 파일이 열람되지 않는다. 정합성 체크를 통해 현재는 undefined로라도 출력되게 해두었다. 이에 대한 디버깅 역시 진행중이다.
🌟 전체적인 구조
🖥 index.html
<!DOCTYPE html>
<html lang="ko">
<link>
<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"></link>
</head>
<body>
<main class="App"></main>
<script src="/src/main.js" type="module"></script>
</body>
</html>
🖥 main.js
import App from './App.js'
const $target = document.querySelector('.App')
new App({ $target })
🖥 api.js
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)
}
}
🖥 Nodes.js
export default function Nodes({ $target, initialState, onClick, onPrevClick}) {
const $nodes = document.createElement('div')
$nodes.classList.add('nodes') // div에 클래스 넣기
$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 && 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
if(!id) {
onPrevClick()
}
const node = this.state.nodes.find(node => node.id === id)
// id가 있는 경우와 없는 경우
if(node) {
onClick(node)
} else {
onPrevClick()
}
})
}
🖥 Loading.js
export default function Loading({$target}) {
const $loading = document.createElement('div')
$target.className = 'Loading Modal'
$target.appendChild($loading)
this.state = false
this.setState = (nextState) => {
this.state = nextState
this.render()
}
this.render = () => {
$loading.innerHTML = `
<div class = "content">
<img width="100%" src= "https://cat-photos-dev-serverlessdeploymentbucket-fdpz0swy5qxq.s3.ap-northeast-2.amazonaws.com/public/images/a2i.jpg" alt="Loading..."/>
</div>
`
$loading.style.display = this.state ? 'block' : 'none'
}
this.render()
}
🖥 imageViewer.js
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'
$imageViewer.innerHTML = `
<div class="content">
<img src="${this.state.selectedImageUrl}"/>
</div>
`
}
this.render()
window.addEventListener('keyup', (e) => {
// ESC를 눌렀을 때 onClose 호출
if(e.key === 'Escape') {
onClose()
}
})
$imageViewer.addEventListener('click', (e) => {
if(Array.from(e.target.classList).includes('Modal')) {
onClose()
}
})
}
🖥 Breadcrumb.js
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 && this.state.map(({id, name}) => `
<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)
})
}
🖥 App.js
import { request } from "./api.js"
import ImageViewer from "./imageViewer.js"
import Nodes from "./Nodes.js"
import Loading from "./Loading.js"
import Breadcrumb from "./Breadcrumb.js"
export default function App ({ $target }) {
this.state = {
isRoot: true,
isLoading: false,
nodes: [],
paths: []
}
const loading = new Loading({
$target
})
const breadcrumb = new Breadcrumb({
$target,
initialState: this.state.paths,
onClick: async (id) => {
// 클릭한 경로 외에 paths 날리기
const nextPaths = id ? [...this.state.paths] : []
const pathIndex = nextPaths.findIndex(path => path.id === id)
if(id) {
const nextPaths = id? [...this.state.paths] : []
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}`
})
}
},
onPrevClick: 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({
imageUrl: 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}` : '/') // id가 있으면 id 기반으로, 없으면 root에서 호출
this.setState({
...this.state,
nodes,
isRoot: id ? false : true,
isLoading: false
})
}
fetchNodes()
}
🖨 구현 결과
아직 오류가 해결 되지 않은 부분 중 대부분이 API의 문제인지라... 상당수는 해결이 어려워보인다.
이미지 문제도 이미지 자체 링크의 문제일지도 모르겠다는 생각이 든다...