diff --git a/mission005/ganeodolu/css/styles.css b/mission005/ganeodolu/css/styles.css
new file mode 100644
index 0000000..c69c8b8
--- /dev/null
+++ b/mission005/ganeodolu/css/styles.css
@@ -0,0 +1,174 @@
+.container {
+ width: 700px;
+ margin: 50px auto 80px;
+}
+
+.header {
+ border: 4px solid #ff9536;
+ border-radius: 14px;
+}
+
+.title {
+ display: inline-block;
+ margin: 10px 20px;
+ font-size: 20px;
+ color: #bd5a00;
+}
+
+.search {
+ display: inline-block;
+ float: right;
+ margin: 7px;
+}
+
+#txt-search {
+ width: 300px;
+ height: 30px;
+ border: 1px solid #ffecd9;
+ border-radius: 7px;
+ background-color: #fff6eb;
+ box-sizing: border-box;
+ vertical-align: middle;
+ outline: none;
+ text-indent: 10px;
+ font-size: 17px;
+}
+
+.btn-search {
+ width: 70px;
+ height: 30px;
+ border: 1px solid #cacaca;
+ border-radius: 7px;
+ background-color: #fff;
+ margin-left: 3px;
+ vertical-align: middle;
+ outline: none;
+ cursor: pointer;
+}
+
+.btn-stored {
+ width: 120px;
+ height: 30px;
+ border: 1px solid #cacaca;
+ border-radius: 7px;
+ background-color: #fff;
+ margin-left: 3px;
+ vertical-align: middle;
+ outline: none;
+ cursor: pointer;
+}
+
+.total {
+ margin: 20px 10px 7px;
+ font-size: 13px;
+ color: #bd5a00;
+}
+
+#item-template {
+ display: none;
+}
+
+.item {
+ padding: 10px 20px;
+ border: 1px solid #d2d2d2;
+ border-radius: 14px;
+ margin-bottom: 10px;
+}
+
+.item-no {
+ display: inline-block;
+ padding-left: 10px;
+ padding-right: 10px;
+ font-size: 30px;
+ color: #a0a0a0;
+ vertical-align: middle;
+ min-width: 17px;
+}
+
+.item-detail {
+ display: inline-block;
+ margin-left: 10px;
+ vertical-align: middle;
+}
+
+.item-name {
+ font-size: 20px;
+}
+
+.item-addr {
+ font-size: 14px;
+ color: #909090;
+ margin-top: 2px;
+}
+
+.paging {
+ text-align: center;
+ margin-top: 23px;
+}
+
+.paging a {
+ text-decoration: none;
+ margin-right: 8px;
+ color: #bd5a00;
+}
+
+.paging a.current {
+ font-weight: bold;
+ color: #ffb500;
+}
+
+.paging a.prev {
+ margin-right: 15px;
+}
+
+.paging a.next {
+ margin-left: 7px;
+}
+
+/* modal */
+.mdl-dialog {
+ border: none;
+ box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
+ width: 500px;
+}
+.mdl-dialog__title {
+ padding: 24px 24px 10;
+ margin: 0;
+ font-size: 2.5rem;
+}
+.mdl-dialog__actions {
+ padding: 8px 8px 8px 24px;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row-reverse;
+ -ms-flex-direction: row-reverse;
+ flex-direction: row-reverse;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.mdl-dialog__actions > * {
+margin-right: 8px;
+height: 36px; }
+.mdl-dialog__actions > *:first-child {
+ margin-right: 0; }
+.mdl-dialog__actions--full-width {
+padding: 0 0 8px 0; }
+.mdl-dialog__actions--full-width > * {
+ height: 48px;
+ -webkit-flex: 0 0 100%;
+ -ms-flex: 0 0 100%;
+ flex: 0 0 100%;
+ padding-right: 16px;
+ margin-right: 0;
+ text-align: right; }
+.mdl-dialog__content {
+ color: rgba(0,0,0, 0.54);
+}
+
+.label {margin-bottom: 96px;}
+.label * {display: inline-block;vertical-align: top;}
+.label .left {background: url("http://t1.daumcdn.net/localimg/localimages/07/2011/map/storeview/tip_l.png") no-repeat;display: inline-block;height: 24px;overflow: hidden;vertical-align: top;width: 7px;}
+.label .center {background: url(http://t1.daumcdn.net/localimg/localimages/07/2011/map/storeview/tip_bg.png) repeat-x;display: inline-block;height: 24px;font-size: 12px;line-height: 24px;}
+.label .right {background: url("http://t1.daumcdn.net/localimg/localimages/07/2011/map/storeview/tip_r.png") -1px 0 no-repeat;display: inline-block;height: 24px;overflow: hidden;width: 6px;}
\ No newline at end of file
diff --git a/mission005/ganeodolu/index.html b/mission005/ganeodolu/index.html
new file mode 100644
index 0000000..1412f14
--- /dev/null
+++ b/mission005/ganeodolu/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
Mission 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/App.js b/mission005/ganeodolu/js/App.js
new file mode 100644
index 0000000..cba7b6f
--- /dev/null
+++ b/mission005/ganeodolu/js/App.js
@@ -0,0 +1,78 @@
+import { apiHandler } from '../util/api.js'
+import SearchStoreName from './SearchStoreName.js'
+import ShowStoreList from './ShowStoreList.js'
+import ShowHistoryList from './ShowHistoryList.js'
+import ManagePage from './ManagePage.js'
+import ShowPage from './ShowPage.js'
+import ManageMap from './ManageMap.js'
+
+function App() {
+ const $targetSearchInput = document.querySelector('#txt-search')
+ const $targetSearchButton = document.querySelector('.btn-search')
+ const $targetDeleteButton = document.querySelector('.btn-delete')
+
+ const searchStoreName = new SearchStoreName({
+ $targetInput: $targetSearchInput,
+ $targetButton: $targetSearchButton,
+ $targetDelete: $targetDeleteButton,
+ onAccessSearch: async (page, keyword) => {
+ const data = await apiHandler({
+ apiPage: page,
+ apiKeyword: keyword
+ })
+ const totalPage = data.total
+ showHistoryList.setState(keyword)
+ showStoreList.setState(data, keyword, page)
+ showPage.setState(page, totalPage)
+ },
+ onClickDelete: () => {
+ showHistoryList.render()
+ }
+ })
+
+ const $targetHistory = document.querySelector('#searched')
+ const showHistoryList = new ShowHistoryList({
+ $target: $targetHistory,
+ data: []
+ })
+
+ const $targetShowResult = document.querySelector('.body')
+ const showStoreList = new ShowStoreList({
+ $target: $targetShowResult,
+ data: [],
+ keyword: [],
+ page: []
+ })
+
+ const $targetShowPage = document.querySelector('.paging')
+ const $targetTotal = document.querySelector('.total')
+ const managePage = new ManagePage({
+ $target: $targetShowPage,
+ $targetHistory: $targetHistory,
+ onClickPage: async (page, keyword) => {
+ const data = await apiHandler({
+ apiPage: page,
+ apiKeyword: keyword
+ })
+ const totalPage = data.total
+ showStoreList.setState(data, keyword, page)
+ showPage.setState(page, totalPage)
+ }
+ })
+ const showPage = new ShowPage({
+ $target: $targetShowPage,
+ $page: [],
+ $totalPage: [],
+ $pageOffset: [],
+ })
+ const $targetMap = document.querySelector('#map')
+ const $targetItem = document.querySelector('.item-detail')
+ const $targetDialog = document.querySelector('#dialog');
+ const manageMap = new ManageMap({
+ $target: $targetMap,
+ $targetItem: $targetShowResult,
+ $targetDialog: $targetDialog,
+ })
+}
+
+new App()
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/ManageMap.js b/mission005/ganeodolu/js/ManageMap.js
new file mode 100644
index 0000000..b60beb2
--- /dev/null
+++ b/mission005/ganeodolu/js/ManageMap.js
@@ -0,0 +1,51 @@
+export default function ManageMap({ $target, $targetDialog, $targetItem }) {
+ this.$target = $target
+ this.$targetDialog = $targetDialog
+ this.$targetItem = $targetItem
+
+ let mapOption = {
+ center: new kakao.maps.LatLng(33.450701, 126.570667),
+ level: 3
+ };
+ let map = new kakao.maps.Map(this.$target, mapOption);
+ let geocoder = new kakao.maps.services.Geocoder();
+
+ this.$targetItem.addEventListener('click', (e) => {
+ if (e.target.closest('div.item')) {
+ let storeName = e.target.closest('div.item').lastElementChild.firstElementChild.textContent
+ let storeAddress = e.target.closest('div.item').lastElementChild.lastElementChild.textContent
+ geocoder.addressSearch(storeAddress, function (result, status) {
+ if (status === kakao.maps.services.Status.OK) {
+ const coords = new kakao.maps.LatLng(result[0].y, result[0].x);
+ const marker = new kakao.maps.Marker({
+ map: map,
+ position: coords
+ });
+ const customOverlay = new kakao.maps.CustomOverlay({
+ position: coords,
+ content: `${storeName}
`
+ })
+ $targetDialog.showModal();
+ setTimeout(function () {
+ map.relayout()
+ customOverlay.setMap(map)
+ map.setCenter(coords);
+ }, 10)
+ }
+ });
+ }
+ })
+
+ this.$targetDialog.querySelector('button')
+ .addEventListener('click', function () {
+ $targetDialog.close();
+ });
+ window.onclick = function (e) {
+ if (e.target === $targetDialog) {
+ $targetDialog.close();
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/ManagePage.js b/mission005/ganeodolu/js/ManagePage.js
new file mode 100644
index 0000000..a7c701b
--- /dev/null
+++ b/mission005/ganeodolu/js/ManagePage.js
@@ -0,0 +1,29 @@
+import { TEXT_NAME } from '../util/constant.js'
+
+export default function ManagePage({ $target, $targetHistory, onClickPage }) {
+ this.$target = $target
+ this.$targetHistory = $targetHistory
+
+ this.$target.addEventListener('click', (e) => {
+ let getKeyword = this.$targetHistory.firstElementChild.value
+ let getPage = e.target.text
+ let getTotal = document.querySelector('.total')
+ getTotal = Number(getTotal.getAttribute('value'))
+ getTotal = Math.floor(getTotal / 10) + 1
+ if (getPage === TEXT_NAME.PREVIOUS) {
+ getPage = Number(e.target.nextElementSibling.text) - 1
+ if (getPage < 1) {
+ getPage = 1
+ }
+ onClickPage(getPage, getKeyword, TEXT_NAME.PREVIOUS)
+ } else if (getPage === TEXT_NAME.NEXT) {
+ getPage = Number(e.target.previousElementSibling.text) + 1
+ if(getPage > getTotal){
+ getPage = getTotal
+ }
+ onClickPage(getPage, getKeyword, TEXT_NAME.NEXT)
+ } else if(getPage) {
+ onClickPage(getPage, getKeyword)
+ }
+ })
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/SearchStoreName.js b/mission005/ganeodolu/js/SearchStoreName.js
new file mode 100644
index 0000000..11a6a37
--- /dev/null
+++ b/mission005/ganeodolu/js/SearchStoreName.js
@@ -0,0 +1,33 @@
+import { KEY_NAME, EVENT_NAME } from '../util/constant.js'
+
+
+export default function SearchStoreName({ $targetInput, $targetButton, $targetDelete, onAccessSearch, onClickDelete }) {
+ this.$targetInput = $targetInput
+ this.$targetButton = $targetButton
+ this.$targetDelete = $targetDelete
+
+ this.$targetInput.addEventListener('keypress', (e) => {
+ if (e.key === KEY_NAME.ENTER) {
+ let keyword = e.target.value
+ onAccessSearch(1, keyword)
+ }
+ })
+
+ this.$targetInput.addEventListener(EVENT_NAME.CLICK, (e) => {
+ let keyword = e.target.value
+ onAccessSearch(1, keyword)
+ })
+
+ this.$targetButton.addEventListener('click', (e) => {
+ $targetInput.dispatchEvent(new Event(EVENT_NAME.CLICK))
+ })
+
+ this.$targetInput.addEventListener('focus', (e) => {
+ e.target.value = ''
+ })
+
+ this.$targetDelete.addEventListener('click', (e) => {
+ localStorage.removeItem('storedKeywords')
+ onClickDelete()
+ })
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/ShowHistoryList.js b/mission005/ganeodolu/js/ShowHistoryList.js
new file mode 100644
index 0000000..660c6a2
--- /dev/null
+++ b/mission005/ganeodolu/js/ShowHistoryList.js
@@ -0,0 +1,37 @@
+import { renderedHistoryHTML } from '../util/template.js'
+import { MESSAGE_NAME } from '../util/constant.js'
+
+export default function ShowHistoryList({ $target, data }) {
+ this.$target = $target
+ this.data = data
+
+ this.render = function () {
+ this.data = this.getHistory()
+ this.$target.innerHTML = renderedHistoryHTML(this.data)
+ }
+ this.setState = function (nextData) {
+ this.data = this.setHistory(nextData)
+ this.render()
+ }
+
+ this.getHistory = function () {
+ const getKeyword = localStorage.getItem('storedKeywords')
+ let keywordList = getKeyword ? getKeyword.split(',') : [`${MESSAGE_NAME.NO_RESULT}`]
+ return keywordList
+ }
+
+ this.setHistory = function (inputValue) {
+ const getKeyword = localStorage.getItem('storedKeywords')
+ let keywordList = getKeyword ? getKeyword.split(',') : []
+ let indexKeyword = keywordList.indexOf(inputValue)
+ if (indexKeyword !== -1) {
+ keywordList.splice(indexKeyword, 1)
+ }
+ keywordList.unshift(inputValue)
+ let result = keywordList.splice(0, 5)
+ localStorage.setItem('storedKeywords', result.toString())
+ return result
+ }
+
+ this.render()
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/ShowPage.js b/mission005/ganeodolu/js/ShowPage.js
new file mode 100644
index 0000000..68f2b85
--- /dev/null
+++ b/mission005/ganeodolu/js/ShowPage.js
@@ -0,0 +1,30 @@
+import { renderedPageHTML } from '../util/template.js'
+import { TEXT_NAME } from '../util/constant.js'
+
+export default function ShowPage({ $target, $page, $totalPage, $pageOffset }) {
+ this.$target = $target
+ this.$page = $page
+ this.$totalPage = $totalPage
+ this.$pageOffset = $pageOffset
+ const numPages = TEXT_NAME.NUM_PAGES
+
+ this.setState = function (nextPage, nextTotalPage, nextPageOffset) {
+ this.$page = nextPage
+ this.$totalPage = nextTotalPage
+ this.$pageOffset = nextPageOffset
+ this.render()
+ }
+ this.render = function () {
+ const maxPage = Math.floor(this.$totalPage / 10) + 1
+ const pageStart = Math.floor((this.$page - 1) / numPages) * numPages + 1
+ let pageEnd = pageStart + numPages - 1
+ if (maxPage < pageEnd) {
+ pageEnd = maxPage
+ }
+ let pageArray = []
+ for (let i = pageStart; i <= pageEnd; i++) {
+ pageArray.push(i)
+ }
+ this.$target.innerHTML = renderedPageHTML(pageArray, this.$page)
+ }
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/js/ShowStoreList.js b/mission005/ganeodolu/js/ShowStoreList.js
new file mode 100644
index 0000000..3ff9d9c
--- /dev/null
+++ b/mission005/ganeodolu/js/ShowStoreList.js
@@ -0,0 +1,24 @@
+import { renderedStoreListHTML } from '../util/template.js'
+
+export default function ShowStoreList({ $target, data, keyword, page }) {
+ this.$target = $target
+ this.data = data
+ this.keyword = keyword
+ this.page = page
+
+
+ this.setState = function (nextData, nextKeyword, nextPage) {
+ this.data = nextData
+ this.keyword = nextKeyword
+ this.page = nextPage
+ this.render()
+ }
+
+ this.render = function () {
+ if (this.data.list) {
+ this.$target.innerHTML = renderedStoreListHTML(this.data, this.keyword, this.page)
+ }
+ }
+
+ this.render()
+}
\ No newline at end of file
diff --git a/mission005/ganeodolu/util/api.js b/mission005/ganeodolu/util/api.js
new file mode 100644
index 0000000..41c9fce
--- /dev/null
+++ b/mission005/ganeodolu/util/api.js
@@ -0,0 +1,15 @@
+import { API_NAME, ERROR_NAME } from './constant.js'
+
+const apiHandler = async ({apiPage, apiKeyword}) => {
+ try {
+ const res = await fetch(`${API_NAME.URI}searchKeyword=${apiKeyword}&perPage=10&page=${apiPage}`)
+ if (res.ok) {
+ const data = await res.json()
+ return data
+ }
+ } catch (error) {
+ throw new Error(ERROR_NAME.NO_ANSWER)
+ }
+}
+
+export { apiHandler }
\ No newline at end of file
diff --git a/mission005/ganeodolu/util/constant.js b/mission005/ganeodolu/util/constant.js
new file mode 100644
index 0000000..64cb453
--- /dev/null
+++ b/mission005/ganeodolu/util/constant.js
@@ -0,0 +1,22 @@
+const API_NAME = {
+ URI: 'https://floating-harbor-78336.herokuapp.com/fastfood?',
+}
+const KEY_NAME = {
+ ENTER: 'Enter'
+}
+const ERROR_NAME = {
+ NO_ANSWER: '서버가 응답하지 않습니다',
+}
+const EVENT_NAME = {
+ CLICK: 'clickSearchButton'
+}
+const MESSAGE_NAME = {
+ NO_RESULT: '최근 검색어가 없습니다'
+}
+const TEXT_NAME = {
+ PREVIOUS: '이전',
+ NEXT: '다음',
+ NUM_PAGES: 5
+}
+
+export { API_NAME, ERROR_NAME, KEY_NAME, EVENT_NAME, MESSAGE_NAME, TEXT_NAME }
\ No newline at end of file
diff --git a/mission005/ganeodolu/util/template.js b/mission005/ganeodolu/util/template.js
new file mode 100644
index 0000000..51755f3
--- /dev/null
+++ b/mission005/ganeodolu/util/template.js
@@ -0,0 +1,41 @@
+function renderedStoreListHTML(inputValue, searchKeyword, countPage) {
+ let result = inputValue.list.map((val, idx) => {
+ return `
+
+
${(countPage - 1) * 10 + idx + 1}
+
+
${val.name}
+
${val.addr}
+
+
+ `
+ }).join('')
+ return `
+ 총 ${inputValue.total}개의 가게를 찾았습니다.
+ ${result}
+ `
+}
+
+function renderedHistoryHTML(inputValue) {
+ let result = inputValue.map((val) => {
+ return `
+
+ `
+ }).join('')
+ return result
+}
+
+function renderedPageHTML(inputValue, currentPage) {
+ let result = inputValue.map((val) => {
+ return `
+ ${val}
+ `
+ }).join('')
+ return `
+ 이전
+ ${result}
+ 다음
+ `
+}
+
+export { renderedStoreListHTML, renderedHistoryHTML, renderedPageHTML }
\ No newline at end of file