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