diff --git a/mission004/ganeodolu/css/style.css b/mission004/ganeodolu/css/style.css
new file mode 100644
index 0000000..12a5f89
--- /dev/null
+++ b/mission004/ganeodolu/css/style.css
@@ -0,0 +1,247 @@
+* {
+ margin: 0;
+ padding: 0;
+ color: #fff;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ border: 0;
+ font-size: 100%;
+ font-style: normal;
+}
+
+*:focus {
+ outline: none;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+}
+
+body {
+ background-color: #020e2f;
+ color: #fff;
+ height: 100%;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: "";
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+button {
+ display: inline-block;
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+ vertical-align: middle;
+}
+
+button:disabled,
+button[disabled] {
+ cursor: initial;
+}
+
+select {
+ border: none;
+ background-color: transparent;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.header {
+ box-shadow: rgba(0, 0, 0, 0.5) 0px 2px 6px 2px;
+}
+
+.header-cont {
+ height: 78px;
+ max-width: 1200px;
+ padding: 0 24px;
+}
+
+.logo {
+ position: absolute;
+ font-weight: bold;
+ font-size: 20px;
+ top: 26px;
+}
+
+.search-bar {
+ position: absolute;
+ height: 24px;
+ right: 24px;
+ top: 20px;
+ width: 240px;
+}
+
+.input-search {
+ width: 210px;
+ height: 24px;
+ background-color: transparent;
+ border-bottom: 2px solid #fff;
+}
+
+.main {
+ padding: 84px 36px;
+ text-align: center;
+}
+
+.movie-list {
+ text-align: left;
+ margin: 0 auto;
+ width: 1024px;
+}
+
+.movie-list-item {
+ width: 180px;
+ margin: 0 20px 30px 0;
+ height: 340px;
+ display: inline-block;
+ vertical-align: top;
+ position: relative;
+ cursor: pointer;
+}
+
+.movie-list-item:hover {
+ top: 1px;
+ left: 1px;
+}
+
+.movie-list-item:hover .movie-list-dim {
+ opacity: 0.5;
+}
+
+.movie-list-dim {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: -webkit-gradient(
+ linear,
+ left top, left bottom,
+ from(rgba(0, 0, 0, 0.3)),
+ to(rgba(0, 0, 0, 1))
+ );
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 0, 0, 0.3) 0%,
+ rgba(0, 0, 0, 1) 100%
+ );
+ opacity: 0;
+ transition: opacity 0.3s ease-in-out;
+}
+
+.poster {
+ width: 180px;
+ height: 270px;
+ -webkit-box-shadow: 3px 3px 9px 0px rgba(0, 0, 0, 0.4);
+ box-shadow: 3px 3px 9px 0px rgba(0, 0, 0, 0.4);
+}
+
+.movie-info {
+ margin-top: 4px;
+}
+
+.movie-info li {
+ font-size: 14px;
+}
+
+.movie-info li:last-of-type {
+ font-size: 12px;
+ opacity: 0.8;
+}
+
+.movie-title {
+ font-weight: bold;
+ margin-bottom: 4px;
+ font-size: 16px;
+}
+
+/*상세 페이지 */
+.main-detail {
+ display: flex;
+ height: calc(100% - 82px);
+}
+
+.detail-bg {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+ background-image: url(https://image.tmdb.org/t/p/w300/lMZyfDVoxhmfBUNmnNQWvSEL9E3.jpg);
+ background-size: cover;
+ filter: blur(3px);
+ opacity: 0.5;
+ background-position: center center;
+ z-index: -1;
+}
+
+.detail-poster {
+ border-radius: 20px;
+ width: auto;
+ height: 100%;
+}
+
+.detail-info {
+ padding-left: 24px;
+ text-align: left;
+}
+
+.detail-info h1 {
+ font-size: 36px;
+ margin-bottom: 12px;
+ font-weight: 300;
+}
+
+.info-list li {
+ display: inline-block;
+ position: relative;
+ margin-right: 16px;
+}
+
+.info-list li:after {
+ content: '.';
+ position: absolute;
+ top: -6px;
+ right: -12px;
+}
+
+.info-list li:last-of-type:after {
+ content: '';
+}
+
+.info-list .txt-14 {
+ font-size: 14px;
+ opacity: 0.8;
+}
+
+.overview {
+ margin-top: 24px;
+ font-size: 14px;
+ opacity: 0.8;
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/html/detail.html b/mission004/ganeodolu/html/detail.html
new file mode 100644
index 0000000..093bfc0
--- /dev/null
+++ b/mission004/ganeodolu/html/detail.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ MISSION 4
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mission004/ganeodolu/index.html b/mission004/ganeodolu/index.html
new file mode 100644
index 0000000..a2b8a83
--- /dev/null
+++ b/mission004/ganeodolu/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ MISSION 4
+
+
+
+
+
+
+
+
+
+
diff --git a/mission004/ganeodolu/js/app.js b/mission004/ganeodolu/js/app.js
new file mode 100644
index 0000000..73ccf12
--- /dev/null
+++ b/mission004/ganeodolu/js/app.js
@@ -0,0 +1,60 @@
+import { fetchGetMovie, fetchSearchMovie } from '../util/api.js'
+import GetMovieList from './getMovieList.js'
+import SearchMovie from './searchMovie.js'
+import ShowMovieList from './showMovieList.js'
+import ScrollMovieList from './scrollMovieList.js'
+
+export default function App() {
+
+ const $targetMovieList = document.querySelector('.main')
+ const $targetLogo = document.querySelector('.logo')
+ const $targetSearch = document.querySelector('.input-search')
+ const $targetSearchIcon = document.querySelector('.material-icons')
+
+ $targetLogo.addEventListener('click', (e) => {
+ sessionStorage.removeItem('getKeyword')
+ window.location.reload()
+ })
+
+ const getMovieList = new GetMovieList({
+ onLoad: async () => {
+ let keyword = sessionStorage.getItem('getKeyword')
+ if (!keyword) {
+ const data = await fetchGetMovie()
+ showMovieList.setState(data)
+ } else {
+ const data = await fetchSearchMovie(1, keyword)
+ showMovieList.setState(data)
+ }
+ }
+ })
+
+ const showMovieList = new ShowMovieList({
+ $targetMovieList: $targetMovieList,
+ data: [],
+ })
+
+ const searchMovie = new SearchMovie({
+ $targetSearch: $targetSearch,
+ $targetSearchIcon: $targetSearchIcon,
+ onClickSearch: async (keyword) => {
+ const data = await fetchSearchMovie(1, keyword)
+ showMovieList.setState(data)
+ }
+ })
+
+ const scrollMovieList = new ScrollMovieList({
+ onScroll: async (pageNumber, keyword) => {
+ if (!keyword) {
+ const data = await fetchGetMovie(pageNumber)
+ showMovieList.addSetState(data)
+ } else {
+ const data = await fetchSearchMovie(pageNumber, keyword)
+ showMovieList.addSetState(data)
+ }
+ }
+ })
+}
+
+new App()
+
diff --git a/mission004/ganeodolu/js/app_detail.js b/mission004/ganeodolu/js/app_detail.js
new file mode 100644
index 0000000..89d444e
--- /dev/null
+++ b/mission004/ganeodolu/js/app_detail.js
@@ -0,0 +1,46 @@
+import ShowMovieDetail from './showMovieDetail.js'
+import GetMovieDetail from './getMovieDetail.js'
+import { fetchGetMovieDetail } from '../util/api.js'
+import { KEY_NAME } from '../util/constant.js'
+
+export default function App_detail() {
+
+ const $targetDetail = document.querySelector('.main')
+ const $targetLogo = document.querySelector('.logo')
+ const $targetSearch = document.querySelector('.input-search')
+ const $targetSearchIcon = document.querySelector('.material-icons')
+
+
+ $targetLogo.addEventListener('click', (e) => {
+ window.location.href = '../index.html';
+ })
+
+ const getMovieDetail = new GetMovieDetail({
+ onLoad: async () => {
+ let movieId = Number(sessionStorage.getItem('getMovieId'))
+ const data = await fetchGetMovieDetail(movieId)
+ showMovieDetail.setState(data)
+ }
+ })
+
+ const showMovieDetail = new ShowMovieDetail({
+ $targetDetail: $targetDetail,
+ data: []
+ })
+
+ $targetSearch.addEventListener('keypress', (e) => {
+ if (e.key === KEY_NAME.ENTER) {
+ let getKeyword = e.target.value
+ sessionStorage.setItem('getKeyword', getKeyword)
+ window.location.href = '../index.html';
+ }
+ })
+ $targetSearchIcon.addEventListener('click', (e) => {
+ let getKeyword = e.target.previousElementSibling.value
+ sessionStorage.setItem('getKeyword', getKeyword)
+ window.location.href = '../index.html';
+ })
+}
+
+new App_detail()
+
diff --git a/mission004/ganeodolu/js/getMovieDetail.js b/mission004/ganeodolu/js/getMovieDetail.js
new file mode 100644
index 0000000..61b2bfd
--- /dev/null
+++ b/mission004/ganeodolu/js/getMovieDetail.js
@@ -0,0 +1,5 @@
+export default function GetMovieDetail({onLoad}){
+ window.addEventListener('load', (e) => {
+ onLoad()
+ })
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/js/getMovieList.js b/mission004/ganeodolu/js/getMovieList.js
new file mode 100644
index 0000000..08071a8
--- /dev/null
+++ b/mission004/ganeodolu/js/getMovieList.js
@@ -0,0 +1,5 @@
+export default function GetMovieList({ onLoad }){
+ window.addEventListener('load', (e) => {
+ onLoad()
+ })
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/js/scrollMovieList.js b/mission004/ganeodolu/js/scrollMovieList.js
new file mode 100644
index 0000000..aa52b6d
--- /dev/null
+++ b/mission004/ganeodolu/js/scrollMovieList.js
@@ -0,0 +1,19 @@
+export default function ScrollMovieList({ onScroll }) {
+
+ let pageNumber = 1;
+ let scrollTimer;
+
+ window.onscroll = async function (e) {
+
+ if ((window.pageYOffset + document.body.clientHeight) >= document.body.scrollHeight * 0.95) {
+ if (!scrollTimer) {
+ scrollTimer = setTimeout(async function () {
+ scrollTimer = null;
+ pageNumber++
+ let searchKeyword = sessionStorage.getItem('getKeyword')
+ onScroll(pageNumber, searchKeyword)
+ }, 200)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/js/searchMovie.js b/mission004/ganeodolu/js/searchMovie.js
new file mode 100644
index 0000000..7625c99
--- /dev/null
+++ b/mission004/ganeodolu/js/searchMovie.js
@@ -0,0 +1,24 @@
+export default function SearchMovie({ $targetSearch, $targetSearchIcon, onClickSearch }) {
+
+ $targetSearch.addEventListener('keypress', async (e) => {
+ if (e.key === 'Enter') {
+ let searchKeyword = e.target.value
+ if (searchKeyword) {
+ onClickSearch(searchKeyword)
+ sessionStorage.setItem('getKeyword', searchKeyword)
+ }
+ }
+ })
+
+ $targetSearchIcon.addEventListener('click', (e) => {
+ searchKeyword = e.target.previousElementSibling.value
+ if (searchKeyword) {
+ onClickSearch(searchKeyword)
+ sessionStorage.setItem('getKeyword', searchKeyword)
+ }
+ })
+
+ $targetSearch.addEventListener('focus', (e) => {
+ e.target.value = ''
+ })
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/js/showMovieDetail.js b/mission004/ganeodolu/js/showMovieDetail.js
new file mode 100644
index 0000000..9d00b1c
--- /dev/null
+++ b/mission004/ganeodolu/js/showMovieDetail.js
@@ -0,0 +1,16 @@
+import { renderedMovieDetailHTML } from '../util/template.js'
+
+export default function ShowMovieDetail({ $targetDetail, data }) {
+ this.$targetDetail = $targetDetail
+ this.data = data
+
+ this.setState = function (nextData) {
+ this.data = nextData;
+ this.render()
+ }
+
+ this.render = function () {
+ this.$targetDetail.innerHTML = renderedMovieDetailHTML(this.data)
+ document.querySelector('.detail-bg').style.backgroundImage = `url(https://image.tmdb.org/t/p/w300${this.data.backdrop_path})`
+ }
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/js/showMovieList.js b/mission004/ganeodolu/js/showMovieList.js
new file mode 100644
index 0000000..b5540ea
--- /dev/null
+++ b/mission004/ganeodolu/js/showMovieList.js
@@ -0,0 +1,37 @@
+import { renderedMovieListHTML } from '../util/template.js'
+import { ERROR_TYPE } from '../util/constant.js'
+
+export default function ShowMovieList({ $targetMovieList, data }) {
+ this.$targetMovieList = $targetMovieList
+ this.data = data
+
+ this.setState = function (nextData) {
+ this.data = nextData;
+ this.render()
+ }
+ this.render = function () {
+ if (typeof (this.data) !== 'object') {
+ throw new Error(ERROR_TYPE.NOT_OBJECT)
+ }
+ if (this.data.results.length === 0) {
+ this.$targetMovieList.innerHTML = ERROR_TYPE.NO_RESULT
+ }
+ else {
+ this.$targetMovieList.innerHTML = renderedMovieListHTML(this.data)
+ }
+ }
+
+ this.addSetState = function (nextData) {
+ this.data = nextData;
+ this.addRender()
+ }
+ this.addRender = function () {
+ if (typeof (this.data) === 'object' && this.data.results.length > 0) {
+ this.$targetMovieList.insertAdjacentHTML('beforeend', renderedMovieListHTML(this.data))
+ }
+ }
+ $targetMovieList.addEventListener('click', (e) => {
+ let getMovieId = e.target.dataset.movieid
+ sessionStorage.setItem('getMovieId', getMovieId)
+ })
+}
\ No newline at end of file
diff --git a/mission004/ganeodolu/package.json b/mission004/ganeodolu/package.json
new file mode 100644
index 0000000..359e985
--- /dev/null
+++ b/mission004/ganeodolu/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "origin",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC"
+}
diff --git a/mission004/ganeodolu/util/api.js b/mission004/ganeodolu/util/api.js
new file mode 100644
index 0000000..874fdbe
--- /dev/null
+++ b/mission004/ganeodolu/util/api.js
@@ -0,0 +1,27 @@
+import { MOVIE_API } from './constant.js'
+
+function fetchGetMovie(pageNumber) {
+ return new Promise((resolve, reject) => {
+ fetch(`${MOVIE_API.URI}movie/now_playing?${MOVIE_API.KEY}&language=en-US&page=${pageNumber}®ion=KR&include_adult=false`)
+ .then(res => res.json())
+ .then(data => resolve(data))
+ })
+}
+
+function fetchGetMovieDetail(movieId) {
+ return new Promise((resolve, reject) => {
+ fetch(`${MOVIE_API.URI}movie/${movieId}?${MOVIE_API.KEY}&language=en-US`)
+ .then(res => res.json())
+ .then(data => resolve(data))
+ })
+}
+
+function fetchSearchMovie(pageNumber, keyword) {
+ return new Promise((resolve, reject) => {
+ fetch(`${MOVIE_API.URI}search/movie?${MOVIE_API.KEY}&language=en-US&query=${keyword}&page=${pageNumber}&include_adult=false`)
+ .then(res => res.json())
+ .then(data => resolve(data))
+ })
+}
+
+export { fetchGetMovie, fetchGetMovieDetail, fetchSearchMovie }
\ No newline at end of file
diff --git a/mission004/ganeodolu/util/constant.js b/mission004/ganeodolu/util/constant.js
new file mode 100644
index 0000000..febdb3d
--- /dev/null
+++ b/mission004/ganeodolu/util/constant.js
@@ -0,0 +1,15 @@
+const MOVIE_API = {
+ URI: 'https://api.themoviedb.org/3/',
+ KEY: 'api_key=9c75b9a50b9510e81c6bc16c1a41517c'
+}
+
+const KEY_NAME = {
+ ENTER: 'Enter'
+}
+
+const ERROR_TYPE = {
+ NOT_OBJECT: '입력값이 객체가 아닙니다.',
+ NO_RESULT: '<검색 결과가 존재하지 않습니다>'
+}
+
+export { MOVIE_API, KEY_NAME, ERROR_TYPE }
\ No newline at end of file
diff --git a/mission004/ganeodolu/util/template.js b/mission004/ganeodolu/util/template.js
new file mode 100644
index 0000000..46da64d
--- /dev/null
+++ b/mission004/ganeodolu/util/template.js
@@ -0,0 +1,57 @@
+function renderedMovieListHTML(inputValue) {
+ let result = inputValue.results.map((val, idx) => {
+ return `
+
+
+
+
+
+ - ${val.title}
+ - ${val.release_date}
+
+
+
+ `
+ }).join('')
+ return result
+}
+
+function renderedMovieDetailHTML(inputValue) {
+ let genresSum = detailSum(inputValue, 'genres', 'name')
+ let countrySum = detailSum(inputValue, 'production_countries', 'name')
+ return `
+
+
+
+
${inputValue.title}
+
+
${inputValue.overview}
+
+ `
+}
+
+function detailSum(result, prop1, prop2) {
+ let answer = result[prop1].map((val) => {
+ return val[prop2]
+ }).join(' / ')
+ return answer
+}
+
+export { renderedMovieListHTML, renderedMovieDetailHTML }
\ No newline at end of file