Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2단계 - 상세 정보 & UI/UX 개선하기] 센트(김영우) 미션 제출합니다. #69

Merged
merged 39 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5aadd5e
feat: 무한 스크롤 기능 구현
kyw0716 Mar 22, 2023
1cf533a
refactor: 로딩 스켈레톤 성능 개선 (요소 추가 방식 => display 변경 방식)
kyw0716 Mar 22, 2023
36ef1e9
feat: 반응형 디자인 적용
kyw0716 Mar 22, 2023
a0c5819
feat: 선택된 영화 정보 불러오는 기능 구현
kyw0716 Mar 22, 2023
e6333f6
feat: 모달 여는 기능 구현
kyw0716 Mar 23, 2023
2045f52
feat: 모달 닫는 기능 구현
kyw0716 Mar 23, 2023
8d03a92
feat: 모달 반응형 디자인 추가
kyw0716 Mar 23, 2023
0ab90a5
feat: 별점 선택 기능 추가
kyw0716 Mar 23, 2023
dc4a7c6
style: 필요없는 import 삭제
kyw0716 Mar 23, 2023
8c453b7
style: 로컬 스토리지 제어 메서드 이름 변경
kyw0716 Mar 24, 2023
8208eed
refactor: 사용하지 않는 메서드 정리
kyw0716 Mar 24, 2023
c2c6fe4
refactor: 유저 사용성 고려하여 모달 내 별 이미지 크기 확대
kyw0716 Mar 24, 2023
5f8fc8e
fix: 별점 선택 오류 수정
kyw0716 Mar 24, 2023
c10c137
style: 모달 내부 텍스트 스타일 수정
kyw0716 Mar 24, 2023
d695e73
docs: 리드미에 배포 링크 추가
kyw0716 Mar 24, 2023
6299cf9
style: 영화 카드 hover 스타일 추가
kyw0716 Mar 24, 2023
1b04be9
refactor: 정보가 없는 경우에 대한 UI 추가
kyw0716 Mar 24, 2023
8ad3bb1
test: 스켈레톤 테스트 추가
kyw0716 Mar 24, 2023
0133ba0
chore: cypress support 폴더 삭제
kyw0716 Mar 24, 2023
f5459d4
refactor: api 에러 처리 추가
kyw0716 Mar 24, 2023
a167d75
refactor: 영화 디테일 모달 띄우는 방식 변경 popstate => click
kyw0716 Mar 26, 2023
2bcf9ed
refactor: 모달 컴포넌트 분리
kyw0716 Mar 26, 2023
c8e1962
feat: hover로 별점 선택하기 기능 추가
kyw0716 Mar 27, 2023
65638a3
feat: 영화 상세정보 모달 컴포넌트 분리
kyw0716 Mar 27, 2023
c194cc6
style: import 컨벤션 추가
kyw0716 Mar 27, 2023
bb01706
refactor: MovieDetailModal 정보 요청 위치 변경
kyw0716 Mar 27, 2023
f1ce9f8
refactor: Header 컴포넌트 이벤트 바인딩 위치 변경
kyw0716 Mar 29, 2023
44cbce4
refactor: api 요청 방식 변경
kyw0716 Mar 29, 2023
54bb3b6
refactor: 직관적이지 않은 에러 메세지 수정
kyw0716 Mar 31, 2023
fdab5de
style: 로컬 스토리지 유틸 함수 early return 추가
kyw0716 Mar 31, 2023
6d76b86
refactor: css에서 !important 제거
kyw0716 Mar 31, 2023
a1b203c
style: map함수 축약
kyw0716 Mar 31, 2023
c86d953
style: 메서드명 수정
kyw0716 Mar 31, 2023
118bff8
style: 이미지 import시 "Image" suffix 추
kyw0716 Mar 31, 2023
ef66def
style: 이미지 import시 "Image" suffix 추가
kyw0716 Mar 31, 2023
c737c26
Merge remote-tracking branch 'origin/step2' into step2
kyw0716 Mar 31, 2023
34e0c0a
refactor: 모달 open시 사용되는 modalType 수정
kyw0716 Mar 31, 2023
c8517f2
refactor: 검색 후 input창 리셋 => 리셋 시키지 않고, input창 클릭시 전체 텍스트 선택으로 수정
kyw0716 Mar 31, 2023
b5f09e8
refactor: 별점 0점 선택 기능 추가
kyw0716 Mar 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# javascript-movie-review

FE 5기 레벨1 영화관 미션
## [배포 링크](https://kyw0716.github.io/javascript-movie-review/)
1 change: 1 addition & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export default defineConfig({
setupNodeEvents(on, config) {
// implement node event listeners here
},
supportFile: false,
},
});
6 changes: 4 additions & 2 deletions cypress/e2e/spec.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ describe("영화 리뷰 웹 테스트", () => {
});
});

it("더보기 버튼 클릭시 다음 페이지 정보가 렌더링 된다.", () => {
cy.get(".btn").click();
it("영화 목록을 마지막 요소까지 스크롤시 다음 페이지 정보가 렌더링 된다.", () => {
cy.scrollTo("bottom");

cy.get(".skeleton-container").should("be.visible");

cy.wait("@getPopularMoviesPage2").then((interception) => {
const movieItems = interception.response?.body.results;
Expand Down
Empty file removed cypress/support/commands.ts
Empty file.
Empty file removed cypress/support/e2e.ts
Empty file.
12 changes: 11 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@
<title>영화 리뷰</title>
</head>
<body>
<div class="modal-section" style="display: none">
<div class="modal-backdrop"></div>
<div class="modal">
<div class="modal-header">
<p class="modal-header--text"></p>
<img class="x-button" alt="close button" />
</div>
<div class="modal-content"></div>
</div>
</div>
<div id="app">
<header></header>
<main>
<section class="item-view">
<h2 class="sub-title">지금 인기 있는 영화</h2>
<ul class="item-list"></ul>
<button class="btn primary full-width">더 보기</button>
<div class="btn"></div>
</section>
</main>
</div>
Expand Down
49 changes: 42 additions & 7 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@
import { Header } from "./components/Header";
import { Modal } from "./components/Modal";
import { MovieList } from "./components/MovieList";
import { $ } from "./utils/selector";

export class App {
#header;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'#header' is declared but its value is never read. 라는 오류를 보여주고 있어요!
어떤 문제가 있는지 한 번 살펴볼까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 나중에 #header를 사용할지도 모른다고 생각해서 그대로 두었는데 결국 사용되지 않아서 생긴 오류인 것 같습니다! 오류를 해결하자고 그냥 #header를 지우기보다 #header가 무언가 하는 역할이 있도록 수정해보는건 어떨까 하고 #header가 입력값을 반환해주는 메서드를 가지도록 수정해보았습니다!

수정한 커밋: f1ce9f8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오,, 아래와 바꿔도 상관없을 거라고 생각했는데, 다른 방법을 사용하셨군요~

new Header(
      $header,
      this.onSubmitSearchKeyword.bind(this),
      this.onClickLogoImage.bind(this)
  );

#movieList;
#modal;

constructor() {
const $header = $("header");
const $movieList = $(".item-list");
const $modal = $(".modal-content");

this.#header = new Header(
$header,
this.onSubmitSearchKeyword.bind(this),
this.onClickLogoImage.bind(this)
);
this.#header = new Header($header);

this.#movieList = new MovieList($movieList);

this.#modal = new Modal($modal);

this.bindEvent($movieList, $header);
}

bindEvent($movieList: Element, $header: Element) {
$movieList.addEventListener("click", ({ target }) => {
if (!(target instanceof HTMLElement)) return;

const movieCard = target.closest("li");
const movieId = movieCard?.dataset.movieId;

if (movieId) this.onClickMovieCard(Number(movieId));
});

$header.addEventListener("click", ({ target }) => {
if (!(target instanceof HTMLImageElement)) return;

this.onClickLogoImage();
});

$header.addEventListener("submit", (event) => {
event.preventDefault();

this.onSubmitSearchKeyword(this.#header.getInputValue());
});
}

onSubmitSearchKeyword(serachKeyword: string) {
const subTitle = $(".sub-title");

if (serachKeyword === "") return alert("검색값을 입력해주세요.");
if (serachKeyword.trim().length === 0)
return alert("올바른 검색어를 입력해주세요.");

subTitle.innerHTML = `"${serachKeyword}" 검색 결과`;

if (this.#movieList instanceof MovieList)
this.#movieList.reset("search", serachKeyword);
this.#movieList.changeShowTarget("search", serachKeyword);
}

onClickLogoImage() {
const subTitle = $(".sub-title");

subTitle.innerHTML = `지금 인기 있는 영화`;

if (this.#movieList instanceof MovieList) this.#movieList.reset("popular");
if (this.#movieList instanceof MovieList)
this.#movieList.changeShowTarget("popular");
}

onClickMovieCard(movieId: number) {
this.#modal.open("movieDetail", movieId);
}
}
27 changes: 27 additions & 0 deletions src/components/Header/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
header h1 {
cursor: pointer;
user-select: none;
font-size: 2rem;
font-weight: bold;
letter-spacing: -0.1rem;
color: #f33f3f;
}

header > .search-box {
background: #fff;
padding: 8px;
border-radius: 4px;
}

header .search-box > input {
border: 0;
}

header .search-box > .search-button {
width: 14px;
border: 0;
text-indent: -1000rem;
background: url("../../../templates/search_button.png") transparent no-repeat
0 1px;
background-size: contain;
}
49 changes: 24 additions & 25 deletions src/components/Header/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
import "./index.css";

import logoImage from "../../../templates/logo.png";

import { $ } from "../../utils/selector";

export class Header {
#$target;

constructor(
$target: Element,
onSubmitSearchKeyword: (searchKeyword: string) => void,
onClickLogoImage: () => void
) {
constructor($target: Element) {
this.#$target = $target;

this.render();

$(".search-box").addEventListener("submit", (event: Event) => {
event.preventDefault();

const $searchInput = $(".search-input");

if ($searchInput instanceof HTMLInputElement) {
const inputValue = $searchInput.value;

if (inputValue === "") return alert("검색값을 입력해주세요.");
if (inputValue.trim().length === 0)
return alert("올바른 검색어를 입력해주세요.");

onSubmitSearchKeyword(inputValue);
}

if (event.target instanceof HTMLFormElement) event.target.reset();
});

$(".logo").addEventListener("click", onClickLogoImage);
this.bindEvent();
}

render() {
Expand All @@ -43,4 +23,23 @@ export class Header {
</form>
`;
}

bindEvent() {
const $searchInput = $(".search-input");

if (!($searchInput instanceof HTMLInputElement)) return;

$searchInput.addEventListener("click", function () {
this.select();
});
}

getInputValue() {
const $searchInput = $(".search-input");

if (!($searchInput instanceof HTMLInputElement))
throw new Error("입력창이 존재하지 않습니다.");

return $searchInput.value;
}
}
34 changes: 34 additions & 0 deletions src/components/Modal/MovieDetailModal/Description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { MovieDetail } from "../../../types";

import filledStarImage from "../../../../templates/star_filled.png";

import { getStarSelectContainerTemplate } from "./StarSelect";

export function getDescriptionTemplate(movie: MovieDetail, starRate: number) {
return /*html*/ `
<div class="modal-detail-container">
<div class="modal-movie-detail">
<p class="modal-movie-genre modal-detail--text">
${
movie.genre.length === 0
? `장르 정보 없음`
: movie.genre.join(" ")
}
<span>
<img
src="${filledStarImage}"
alt="별점 ${movie.vote_average}"
/>
${movie.vote_average.toFixed(1)}
</span>
</p>
<p class="modal-movie-description modal-detail--text">
${movie.overview ? movie.overview : "상세 정보 없음"}
</p>
</div>
<div class="modal-star-rate modal-detail--text">
${getStarSelectContainerTemplate(movie.id, starRate)}
</div>
</div>
`;
}
31 changes: 31 additions & 0 deletions src/components/Modal/MovieDetailModal/Image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function getImageContainerTemplate(imagePath: string, title: string) {
return /*html*/ `
<div class="modal-image-container">
${
imagePath
? /*html */
`<img
class="modal-image skeleton"
src="https://image.tmdb.org/t/p/w220_and_h330_face/${imagePath}"
alt="${title} 포스터"
/>`
: /*html */
`<div
class="modal-image center"
style="
background-color:white;
color:black;
display:flex;
justify-content:center;
align-items:center;
font-weight:600;
font-size:24px;
border-radius: 16px;
"
>
<span>No Image</span>
</div>`
}
</div>
`;
}
59 changes: 59 additions & 0 deletions src/components/Modal/MovieDetailModal/StarSelect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import filledStarImage from "../../../../templates/star_filled.png";
import emptyStarImage from "../../../../templates/star_empty.png";

import { $ } from "../../../utils/selector";

const STAR_RATE_STRING = [
"나의 점수는?",
"최악이예요",
"별로예요",
"보통이예요",
"재미있어요",
"명작이예요",
];

export function renderStars(movieId: number, starRate: number) {
const starRateContainer = $(".modal-star-rate");

if (starRateContainer instanceof HTMLElement)
starRateContainer.innerHTML = getStarSelectContainerTemplate(
movieId,
starRate
);
}

export function getStarSelectContainerTemplate(
movieId: number,
starRate: number
) {
const imgArray = getStarTemplate(movieId, starRate);

return /*html*/ `
<span
class="select-zero-rate"
data-movie-id="${movieId}"
data-star-rate="0"
>
내 별점
</span>
<span class="star-select-container">
${imgArray.join("")}
</span>
<span>${starRate * 2}점</span>
<span class="star-rate-desc">${STAR_RATE_STRING[starRate]}</span>
`;
}

export function getStarTemplate(movieId: number, starRate: number) {
return Array.from(
{ length: 5 },
(_, i) =>
`<img
src="${starRate > i ? filledStarImage : emptyStarImage}"
alt="별점"
class="star-rate-select-img"
data-movie-id="${movieId}"
data-star-rate="${i + 1}"
/>`
);
}
Loading