Skip to content

Commit

Permalink
feat: 통신에 실패할 경우 오류를 발생시키고, 해당 오류가 발생한 경우 오류 메세지를 띄워주는 기능 구현
Browse files Browse the repository at this point in the history
Co-authored-by: cys4585 <[email protected]>
  • Loading branch information
Hain-tain and cys4585 committed Mar 21, 2024
1 parent 89ac917 commit 57f874a
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 25 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

## 기능 목록

- [ ] Domain
- [x] Domain

- [x] `인기순`으로 영화 목록 불러온다.
- 한 번에 20개씩 불러온다.
- 별도로 받은 데이터를 정렬하지 않는다.
- [ ] 통신에 실패한 경우 오류를 발생시킨다.
- [x] 통신에 실패한 경우 오류를 발생시킨다.
- [x] 인자로 받은 쿼리로 영화 목록을 불러온다.
- 한 번에 20개씩 불러온다.
- [ ] 통신에 실패한 경우 오류를 발생시킨다.
- [x] 통신에 실패한 경우 오류를 발생시킨다.

- [ ] UI
- [x] UI

- [x] 영화 목록의 1페이지(20개)를 출력한다.

Expand All @@ -29,4 +29,4 @@
- [x] 단, 페이지 끝에 도달한 경우에는 더보기 버튼을 화면에 출력하지 않는다.

- [x] 데이터를 로딩하는 동안 영화 목록 아이템에 대한 Skeleton UI를 출력한다.
- [ ] 오류가 발생한 경우, 오류 메세지를 띄워준다.
- [x] 오류가 발생한 경우, 오류 메세지를 띄워준다.
55 changes: 42 additions & 13 deletions src/components/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,53 @@ class App {
}

private async renderPopularMovieList() {
const res = await getPopularMovieList({ page: this.currentPage });
const movies = this.extractMovies(res.results);
setTimeout(() => {
this.movieMain.reRender(movies);
}, 500);
try {
const res = await getPopularMovieList({ page: this.currentPage });

if (this.currentPage === res.total_pages) {
this.movieMain.removeMovieMoreButton();
}

const movies = this.extractMovies(res.results);
setTimeout(() => {
this.movieMain.reRender(movies);
}, 500);
} catch (error) {
if (error instanceof Error) {
this.currentPage -= 1;
this.movieMain.renderMessage(error.message);
this.movieMain.reRender([]);
}
}
}

private async searchMovies(query: string) {
const res = await getMovieListByQuery({ page: this.currentPage, query });
const movies = this.extractMovies(res.results);

if (this.currentPage === res.total_pages) {
this.movieMain.removeMovieMoreButton();
try {
const res = await getMovieListByQuery({ page: this.currentPage, query });
const movies = this.extractMovies(res.results);
if (!movies.length) {
this.renderNoResult("검색 결과가 없습니다.");
}

if (this.currentPage === res.total_pages) {
this.movieMain.removeMovieMoreButton();
}

setTimeout(() => {
this.movieMain.reRender(movies);
}, 500);
} catch (error) {
if (error instanceof Error) {
this.currentPage -= 1;
this.renderNoResult(error.message);
this.movieMain.reRender([]);
}
}
}

setTimeout(() => {
this.movieMain.reRender(movies);
}, 500);
private renderNoResult(message: string) {
this.movieMain.removeMovieMoreButton();
this.movieMain.renderMessage(message);
}

private extractMovies(movies: SearchMovieResult[] | PopularMovieResult[]) {
Expand Down
18 changes: 17 additions & 1 deletion src/components/MovieHeader/MovieSearchBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,25 @@ class MovieSearchBox {
private searchByQuery(e: Event) {
e.preventDefault();

if (e.target instanceof HTMLFormElement) {
if (!(e.target instanceof HTMLFormElement)) {
return;
}

try {
const query = e.target["query"].value;
this.validateQuery(query);
this.props.searchBoxSubmitHandler(query);
} catch (error) {
if (error instanceof Error) alert(error.message);
}
}

private validateQuery(query: string) {
if (query.trim().length === 0) {
throw new Error("검색어를 입력해주세요.");
}
if (query.length > 500) {
throw new Error("검색어는 500자 미만으로 입력해주세요.");
}
}
}
Expand Down
42 changes: 38 additions & 4 deletions src/components/MovieMain/MovieListBox/MovieList/MovieList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import MovieItem, { Movie } from "./MovieItem";
import createElement from "../../../utils/createElement";

class MovieList {
private static MAX_ITEM_OF_PAGE = 20;
$element;
movieList;

constructor() {
this.movieList = Array.from({ length: 20 }).map(() => new MovieItem());
this.movieList = Array.from({ length: MovieList.MAX_ITEM_OF_PAGE }).map(
() => new MovieItem()
);
this.$element = this.generateMovieList();
}

Expand All @@ -19,9 +22,8 @@ class MovieList {
});
}

//TODO: 20개가 전부 오지 않았을 경우 요소 remove
reRender(movies: Movie[]) {
Array.from({ length: 20 }).forEach((_, index) => {
Array.from({ length: MovieList.MAX_ITEM_OF_PAGE }).forEach((_, index) => {
const movieItem = this.movieList[index];

if (index < movies.length) {
Expand All @@ -35,9 +37,41 @@ class MovieList {
}

appendSkeleton() {
this.movieList = Array.from({ length: 20 }).map(() => new MovieItem());
this.movieList = Array.from({ length: MovieList.MAX_ITEM_OF_PAGE }).map(
() => new MovieItem()
);
this.$element.append(...this.movieList.map((item) => item.$element));
}

removeAllSkeleton() {
this.movieList.forEach((movieItem) => {
movieItem.$element.remove();
});
}

renderMessage(message: string) {
this.removeAllSkeleton();

const textInvalidResult = createElement({
tagName: "div",
attribute: {
class: "text-invalid-result",
},
children: [message],
});

this.$element.append(textInvalidResult);
}

removeMessage() {
const $lastChild = this.$element.lastChild;
if (!($lastChild instanceof HTMLElement)) {
return;
}
if ($lastChild.classList.contains("text-invalid-result")) {
$lastChild.remove();
}
}
}

export default MovieList;
5 changes: 5 additions & 0 deletions src/components/MovieMain/MovieListBox/MovieListBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class MovieListBox {
this.movieList = new MovieList();
this.button = new MovieMoreButton({
onClickHandler: () => {
this.movieList.removeMessage();
this.showMoreMovies();
onMovieMoreButtonClick();
},
Expand All @@ -38,6 +39,10 @@ class MovieListBox {
this.button.toggleDisabled();
}

renderMessage(message: string) {
this.movieList.renderMessage(message);
}

showMoreMovies() {
this.button.toggleDisabled();
this.movieList.appendSkeleton();
Expand Down
4 changes: 4 additions & 0 deletions src/components/MovieMain/MovieMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class MovieMain {
this.movieListBox.removeMovieMoreButton();
}

renderMessage(message: string) {
this.movieListBox.renderMessage(message);
}

private replace(movieListBoxElement: HTMLElement) {
this.$element.replaceChildren(movieListBoxElement);
}
Expand Down
4 changes: 4 additions & 0 deletions src/domain/getMovieListByQuery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import validateResponse from "./validateResponse";

const getMovieListByQuery = async ({
page,
query,
Expand All @@ -10,6 +12,8 @@ const getMovieListByQuery = async ({
const moviesUrl = `${url}?${queryParams}`;

const res = await fetch(moviesUrl);
validateResponse(res.status);

const movieList = await res.json();

return movieList;
Expand Down
6 changes: 4 additions & 2 deletions src/domain/getPopularMovieList.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import validateResponse from "./validateResponse";

const getPopularMovieList = async ({
page,
}: {
page: number;
}): Promise<PopularMovieResponse> => {
const options = { method: "GET", headers: { accept: "application/json" } };

const url = "https://api.themoviedb.org/3/movie/popular";
const queryParams = `language=ko-KR&page=${page}&api_key=${process.env.API_KEY}`;
const popularMoviesUrl = `${url}?${queryParams}`;

const res = await fetch(popularMoviesUrl);
validateResponse(res.status);

const popularMovieList = await res.json();

return popularMovieList;
Expand Down
35 changes: 35 additions & 0 deletions src/domain/validateResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const validateResponse = (status: number) => {
if (status === 401) {
throw new Error("인증 정보가 올바르지 않습니다. API KEY를 확인해주세요.");
}
if (status === 503) {
throw new Error(
"죄송합니다. 현재 서버가 일시적으로 오프라인 상태입니다. 나중에 다시 시도해주세요."
);
}
if (500 <= status) {
throw new Error(
"죄송합니다. 서버가 점검중입니다. 다음에 다시 시도해주세요."
);
}
if (400 <= status) {
throw new Error("알 수 없는 오류가 발생했습니다. 다시 시도해주세요.");
}
};

export default validateResponse;

// TMDB 에서 올 수 있는 모든 ERROR 목록
// 400: "사용자 이름과 비밀번호를 제공해야 합니다. 잘못된 페이지, 날짜형식입니다. validation failed. 검증실패.",
// 401: "API Key가 없습니다. API Key가 일시 정지됐습니다.",
// 403: "이 사용자는 정지되었습니다. 제출하려는 데이터가 이미 존재합니다.",
// 404: "잘못된 아이디거나 찾을 수 없습니다. ",
// 405: "이 서비스는 그 형식이 아닙니다. 잘못된 형식. 해당 형식이 존재하지 않습니다.",
// 406: "잘못 승인된 헤더입니다.",
// 422: "잘못된 날짜 범위(14일 이하여야 합니다). invalid parameter. 요청의 파라미터가 올바르지 않습니다.",
// 429: "요청 횟수가 허용 한도(40회)를 초과했습니다.",
// 500: "default server error. 내부 오류. 문제 발생. TMDB에 문의해주세요.",
// 501: "서버가 존재하지 않습니다.",
// 502: "백엔드 서버에 연결할 수 없습니다.",
// 503: "이 API가 점검 중입니다. 이 서비스는 일시적으로 오프라인 상태입니다. 나중에 다시 시도하세요.",
// 504: "백엔드 서버에 대한 요청 시간 초과. 다시 시도하세요.",
9 changes: 9 additions & 0 deletions templates/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,12 @@ header .search-box > .search-button {
background: url("./search_button.png") transparent no-repeat 0 1px;
background-size: contain;
}

.text-invalid-result {
color: #aaa;
font-size: 1.5rem;
grid-column-start: 1;
grid-column-end: 5;
text-align: center;
margin-top: 2rem;
}

0 comments on commit 57f874a

Please sign in to comment.