From 7dc9b8cfeb18252030fe2796493dd05073127398 Mon Sep 17 00:00:00 2001 From: gaaahee Date: Thu, 7 Nov 2024 20:13:00 +0900 Subject: [PATCH] =?UTF-8?q?keyword:=205=EC=A3=BC=EC=B0=A8=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyword/chapter05/keyword.md | 257 ++++++++++++++++++ mission/chapter04/mission_ch04/src/App.jsx | 28 +- .../src/components/CastCard/CastCard.jsx | 3 +- .../components/CategoryCard/CategoryCard.jsx | 27 +- .../CategoryCard/CategoryCard.style.jsx | 23 ++ .../src/components/Navbar/navbar.jsx | 2 +- .../{navbar.styled.js => navbar.style.jsx} | 0 .../src/components/Sidebar/sidebar.jsx | 2 +- .../{sidebar.styled.js => sidebar.style.jsx} | 5 +- .../mission_ch04/src/hooks/useCustomFetch.js | 9 +- .../mission_ch04/src/layout/root-layout.jsx | 6 +- .../src/layout/root-layout.styled.js | 1 - .../src/pages/CategoryList/CategoryList.jsx | 24 +- ...yList.styled.js => CategoryList.style.jsx} | 32 ++- .../chapter04/mission_ch04/src/pages/ID.jsx | 12 - .../src/pages/MovieList/MovieList.jsx | 40 --- .../MovieDetail/MovieDetail.css | 0 .../MovieDetail/MovieDetail.jsx | 15 +- .../pages/MoviePages/MovieList/MovieList.jsx | 41 +++ .../MovieList/MovieList.style.jsx} | 7 +- .../src/pages/MoviePages/MovieRoute.jsx | 14 + 21 files changed, 416 insertions(+), 132 deletions(-) create mode 100644 keyword/chapter05/keyword.md create mode 100644 mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.style.jsx rename mission/chapter04/mission_ch04/src/components/Navbar/{navbar.styled.js => navbar.style.jsx} (100%) rename mission/chapter04/mission_ch04/src/components/Sidebar/{sidebar.styled.js => sidebar.style.jsx} (81%) rename mission/chapter04/mission_ch04/src/pages/CategoryList/{CategoryList.styled.js => CategoryList.style.jsx} (57%) delete mode 100644 mission/chapter04/mission_ch04/src/pages/ID.jsx delete mode 100644 mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.jsx rename mission/chapter04/mission_ch04/src/pages/{ => MoviePages}/MovieDetail/MovieDetail.css (100%) rename mission/chapter04/mission_ch04/src/pages/{ => MoviePages}/MovieDetail/MovieDetail.jsx (66%) create mode 100644 mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.jsx rename mission/chapter04/mission_ch04/src/pages/{MovieList/MovieList.css => MoviePages/MovieList/MovieList.style.jsx} (54%) create mode 100644 mission/chapter04/mission_ch04/src/pages/MoviePages/MovieRoute.jsx diff --git a/keyword/chapter05/keyword.md b/keyword/chapter05/keyword.md new file mode 100644 index 0000000..469a9d0 --- /dev/null +++ b/keyword/chapter05/keyword.md @@ -0,0 +1,257 @@ +### 키워드 정리 🍠 + +- useRef 🍠 + + `useRef`는 컴포넌트 내에서 DOM 요소나 특정 값을 저장하고 관리할 때 사용됨 + + 1. **DOM 요소 접근**: 예를 들어, 특정 `` 필드에 포커스를 주고 싶을 때 `useRef`로 해당 요소에 접근할 수 있다. + - **폼 초기 포커스**: 페이지 로드 시 자동으로 첫 번째 필드에 포커스를 주고 싶을 때 + - **유효성 검사 실패 시**: 입력값이 잘못된 경우, 해당 필드로 포커스를 이동해 사용자에게 쉽게 위치를 알려줄 때 + - **사용자 경험 개선**: 사용자가 입력할 때마다 필요한 필드로 자연스럽게 포커스를 이동시키고 싶을 때 + 2. **값 저장 (리렌더링 없이 유지)**: useRef는 리렌더링 없이 값을 저장한다. `useState` 또한 값을 저장할 수 있지만, 값이 변화하면 리렌더링이 발생한다. `useRef` 는 UI에 반영하지 않고 값을 저장하거나 유지해야 할 때, `useState`는 UI에 실시간으로 반영해야 되는 값을 관리할 때 사용하기 적절하다. + 3. **타이머나 애니메이션**: `setTimeout`, `setInterval` 같은 타이머 ID나 애니메이션 프레임을 참조할 때에도 `useRef`를 사용해 관리할 수 있습니다. +- input의 주요 프로퍼티 🍠 + - 아래 내용 이외에, 자주 사용하는 프로퍼티가 있으면 추가로 더 정리해주세요! 🍠 + + ### 1. `type` + + - **설명**: 입력 필드의 종류를 설정합니다. + - **값 예시**: `"text"`, `"password"`, `"email"`, `"number"`, `"checkbox"`, `"radio"` 등 + - **예시**: `` + + ### 2. `value` + + - **설명**: 입력 필드의 값을 설정하고 제어할 때 사용됩니다. **Controlled Component**에서 자주 사용됩니다. + - **값 예시**: 문자열 또는 숫자 + - **예시**: `` + + ### 3. `defaultValue` + + - **설명**: 초기값을 설정하는 데 사용됩니다. + - **값 예시**: 문자열 또는 숫자 + - **예시**: `` + + ### 4. `onChange` + + - **설명**: 사용자가 입력 필드에 값을 입력하거나 변경할 때 호출되는 이벤트 핸들러입니다. + - **값 예시**: 함수 + - **예시**: ` setValue(e.target.value)} />` + + ### 5. `placeholder` + + - **설명**: 입력 필드가 비어 있을 때 표시되는 힌트 텍스트입니다. + - **값 예시**: 문자열 + - **예시**: `` + + ### 6. `checked` + + - **설명**: 체크박스나 라디오 버튼이 선택되었는지 여부를 제어합니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 7. `defaultChecked` + + - **설명**: 체크박스나 라디오 버튼의 초기 상태를 설정합니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 8. `disabled` + + - **설명**: 입력 필드를 비활성화하여 사용자 입력을 막습니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 9. `readOnly` + + - **설명**: 입력 필드의 값을 읽기 전용으로 설정합니다. 사용자는 값을 변경할 수 없습니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 10. `name` + + - **설명**: 폼 데이터를 제출할 때 서버로 전송되는 데이터의 이름을 지정합니다. + - **값 예시**: 문자열 + - **예시**: `` + + ### 11. `maxLength` + + - **설명**: 입력할 수 있는 최대 글자 수를 지정합니다. + - **값 예시**: 숫자 + - **예시**: `` + + ### 12. `min` / `max` + + - **설명**: 숫자 또는 날짜 입력에서 사용할 수 있는 최소/최대 값을 지정합니다. + - **값 예시**: 숫자 또는 날짜 + - **예시**: `` + + ### 13. `autoFocus` + + - **설명**: 페이지가 로드될 때 자동으로 입력 필드에 포커스를 줍니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 14. `required` + + - **설명**: 입력 필드를 필수 입력으로 설정합니다. 폼을 제출할 때 이 필드가 비어 있으면 제출이 거부됩니다. + - **값 예시**: `true` 또는 `false` + - **예시**: `` + + ### 15. **`size`** + + - **설명**: 텍스트 입력의 표시되는 크기를 지정합니다. 실제 입력 길이를 제한하지는 않지만, 사용자에게 보이는 텍스트 필드의 길이를 조절할 수 있습니다. + - **값 예시**: 숫자 (필드의 문자 길이) + - **예시**: `` + + ### 16. `pattern` + + - **설명**: 정규 표현식으로 입력값의 형식을 제한할 수 있습니다. 예를 들어, 전화번호 형식 또는 이메일 형식만 허용하고 싶을 때 유용합니다. + - **값 예시**: 정규 표현식 (`"^\d{10}$"` 등) + - **예시**: `` + +### **`Controlled Input`** vs **`UnControlled Input`** + +- Controlled Input + + ### Controlled Input + + **`Controlled Input`**은 React의 상태(state)에 의해 폼의 값을 관리하는 방식입니다. Input의 value가 컴포넌트의 상태에 연결되어 있고, 상태값이 달라질 떄 마다, 입력 필드의 값도 갱신됩니다. + + 쉽게 말해서, Input의 값은, **`React Component가 제어`**하고, 모든 입력 변화 또한, **`컴포넌트의 상태로 반영`**되어집니다. + + 특징 + + - Input의 value는, React의 **`state`**로 관리됩니다. + - 상태 값이, **`Input의 value 속성에 직접적으로 연결`** 됨. + - **`Input`**의 **`Value`**가 **`변경`**되면, **`상태가 업데이트`**되고, **`상태가 다시 렌더링을 Trigger`** 함. + - **`React`**가 **`Form 요소의 현재 값을 항상 알고 있기에 제어가 용이`**. + + ```tsx + import { useState } from 'react'; + + function ControlledInput() { + const [inputValue, setInputValue] = useState(''); + + const handleChange = (event) => { + setInputValue(event.target.value); + }; + + return ( +
+ +

입력: {inputValue}

+
+ ); + } + + export default ControlledInput; + + ``` + + ### 장점: + + - 상태를 통해 값이 직접적으로 제어되기 때문에 **`폼 데이터를 검증`**하거나 **`조작`**하기가 쉽습니다. + - **`사용자 입력을 실시간으로 검증`**하거나 **`포맷을 조정`**할 수 있습니다. + + ### 단점: + + - **`컴포넌트에서 상태 관리가 복잡`**해질 수 있으며, 특히 폼 데이터에 너무 많은 value들을 관리하는 경우 **`성능에 부담`**이 될 수 있습니다. + +- UnControlled Input + + ### UnControlled Input + + **`Uncontrolled Input`**은 React의 state가 아닌 DOM 자체에서 입력 값을 관리하는 방식입니다. 폼의 값은 **`React 컴포넌트가 직접 관리하지 않고`**, 필요할 때 DOM에서 직접 값을 참조하는 방법입니다. 이를 위해 `ref`를 사용하여 **`DOM 요소에 직접 접근`**할 수 있습니다. + + ### 특징 + + - 입력값이 `state`에 의존하지 않고, React의 제어 밖에서 관리됩니다. + - 폼 요소의 값은 사용자가 입력하는 대로 DOM에 저장되며, 필요할 때만 값을 읽어 옵니다. + - React의 상태를 사용하지 않기 때문에 폼을 간단하게 유지할 수 있습니다. + + ```tsx + import { useRef } from 'react'; + + function UncontrolledInput() { + const inputRef = useRef(null); + + const handleSubmit = () => { + console.log(`입력: ${inputRef.current.value}`); + }; + + return ( +
+ + +
+ ); + } + + export default UncontrolledInput; + + ``` + + ### 장점 + + - 상태 관리가 필요 없으므로 간단한 폼에 적합하며, **`성능적으로도 유리`**할 수 있습니다. + - 작은 폼이나 **`상태가 필요 없는 경우 사용하기 쉽습니다.`** + + ### 단점 + + - 입력 값을 **`실시간으로 검증하거나 조작하기 어려우며`**, 폼 데이터와 관련된 논리를 관리하는 데 제약이 있을 수 있습니다. + - **`DOM에 직접 접근`**하기 때문에 **`React의 단방향 데이터 흐름을 벗어나는 경우`**가 생길 수 있습니다. + + +### react-hook-form & yup 🍠 + +**`react-hook-form`**과 **`yup`**은 React에서 폼을 간편하게 관리하고, 유효성 검사를 수행하는 데 유용한 라이브러리 입니다. 두 개의 라이브러리를 함께 사용하면, 폼 입력 관리와 검증 과정을 매우 효율적으로 처리할 수 있습니다. + +현재, 여기서는 **`yup`**을 통한, **`validation`**을 설명하지만, 혹시라도, 본인이 작업하시는 프로젝트가 **`TypeScript`**시라면, **`zod`**를 사용하는 것이 조금 더 유리할 수도 있으니, **`zod`**를 더욱 추천드립니다! + +- **`react-hook-form`** 공식문서 설명 + + ### react-hook-form + + https://react-hook-form.com/get-started + + 주요 특징 + + 1. DX (개발자 경험) + + 직관적이고, 기능이 완벽한 API로, 개발자가 폼을 구축할 떄 매끄러운 경험을 제공합니다. + + 2. HTML 표준 + + 기존의 HTML 마크업을 활용하여 제약 기반 검증 API로 폼을 검증합니다. + + 3. 가벼움 + + 패키지 크기는 매우 중요합니다. React Hook Form은 의존성이 없는 작은 라이브러리이다. + + 4. 성능 + + 리렌더링 횟수를 최소화하고, 검증 계산을 줄이며, 더 빠른 마운팅을 제공. + + 5. 채택 가능성 + + 폼 상태는 본질적으로 로컬에 있기 떄문에 다른 의존성 없이도 쉽게 채택 가능하다. + + 6. UX (사용자 경험) + + 최상의 사용자 경험을 제공하기 위해 일관된 검증 전략을 지향. + +- **`yup`** 공식문서 설명 + + ### yup + + ‣ + + **`Yup`**은 런타임 값 파싱 및 검증을 위한 스키마 빌더입니다. 스키마를 정의하고, 값을 변환하여 일치시키거나, 기존 값의 형태를 검증하거나, 두 작업을 모두 수행할 수 있습니다. **`Yup 스키마`**는 매우 표현력이 뛰어나며 복잡하고 상호 의존적인 검증이나 값 변환을 모델링할 수 있습니다. + + 주요 기능: + + 1. 간결하면서도 표현력이 뛰어난 스키마 인터페이스로, 간단한 데이터 모델부터 복잡한 데이터 모델까지 설계 가능 + 2. 강력한 TypeScript 지원: 스키마에서 정적 타입을 추론하거나, 스키마가 올바르게 타입을 구현하는지 확인 + 3. 내장된 비동기 검증 지원: 서버 측 및 클라이언트 측 검증을 동일하게 모델링 가능 + 4. 확장성: 타입 안정성이 보장된 메서드와 스키마를 추가할 수 있음 + 5. 풍부한 오류 세부 정보 제공으로 디버깅이 쉬움 \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/App.jsx b/mission/chapter04/mission_ch04/src/App.jsx index ae43671..9bbbba0 100644 --- a/mission/chapter04/mission_ch04/src/App.jsx +++ b/mission/chapter04/mission_ch04/src/App.jsx @@ -2,40 +2,40 @@ import React from 'react'; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import RootLayout from "./layout/root-layout"; import HomePage from "./pages/Default/home"; -import Login from "./pages/Default/login"; -import Signup from "./pages/Default/signup"; -import Search from './pages/Default/search'; -import CategoryList from "./pages/CategoryList/CategoryList"; -import MovieList from "./pages/MovieList/MovieList"; // 영화 목록 페이지 +import LoginPage from "./pages/Default/login"; +import SignupPage from "./pages/Default/signup"; +import SearchPage from './pages/Default/search'; +import CategoryListPage from "./pages/CategoryList/CategoryList"; +import MovieRoute from "./pages/MoviePages/MovieRoute"; const router = createBrowserRouter([ { path: '/', - element: , + element: , children: [ { index: true, - element: + element: }, { path: 'login', - element: + element: }, { path: 'signup', - element: + element: }, { path: 'search', - element: + element: }, { path: 'movies', - element: + element: }, { - path: 'movies/:movieId', - element: + path: 'movies/:id', + element: } ] } @@ -45,4 +45,4 @@ function App() { return ; } -export default App; +export default App; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/components/CastCard/CastCard.jsx b/mission/chapter04/mission_ch04/src/components/CastCard/CastCard.jsx index e165423..a2beaa9 100644 --- a/mission/chapter04/mission_ch04/src/components/CastCard/CastCard.jsx +++ b/mission/chapter04/mission_ch04/src/components/CastCard/CastCard.jsx @@ -7,7 +7,8 @@ const CastCard = (props) =>{ <>
-
+
{cast.name}

{cast.character}

diff --git a/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.jsx b/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.jsx index f88bff4..1e21e55 100644 --- a/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.jsx +++ b/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.jsx @@ -1,4 +1,5 @@ -import styled from "styled-components"; +import React from 'react'; +import {CardDiv, CardSpan} from './CategoryCard.style'; const CategoryCard = ({title})=>{ return( @@ -9,26 +10,4 @@ const CategoryCard = ({title})=>{ } -export default CategoryCard; - -const CardDiv = styled.div` - width : 200px; - height : 100px; - background-color : white; - border-radius: 10px; - display : flex; - align-items : flex-end; - &:hover{ - transform: scale(1.05); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); - } -` -const CardSpan = styled.span` - background-color: grey; - color: white; - opacity: 0.5; - border-radius: 5px; - margin :10px 10px 10px 10px; - padding : 0px 3px 3px 3px; - vertical-align : bottom; -` \ No newline at end of file +export default CategoryCard; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.style.jsx b/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.style.jsx new file mode 100644 index 0000000..db0e567 --- /dev/null +++ b/mission/chapter04/mission_ch04/src/components/CategoryCard/CategoryCard.style.jsx @@ -0,0 +1,23 @@ +import styled from "styled-components"; + +export const CardDiv = styled.div` + width : 200px; + height : 100px; + background-color : white; + border-radius: 10px; + display : flex; + align-items : flex-end; + &:hover{ + transform: scale(1.05); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); + } +` +export const CardSpan = styled.span` + background-color: grey; + color: white; + opacity: 0.5; + border-radius: 5px; + margin :10px 10px 10px 10px; + padding : 0px 3px 3px 3px; + vertical-align : bottom; +` \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/components/Navbar/navbar.jsx b/mission/chapter04/mission_ch04/src/components/Navbar/navbar.jsx index 801f512..4510dd7 100644 --- a/mission/chapter04/mission_ch04/src/components/Navbar/navbar.jsx +++ b/mission/chapter04/mission_ch04/src/components/Navbar/navbar.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { NavbarContainer, Logo, NavLinks, NavButton } from "./navbar.styled"; +import { NavbarContainer, Logo, NavLinks, NavButton } from "./navbar.style"; const Navbar = () => { return ( diff --git a/mission/chapter04/mission_ch04/src/components/Navbar/navbar.styled.js b/mission/chapter04/mission_ch04/src/components/Navbar/navbar.style.jsx similarity index 100% rename from mission/chapter04/mission_ch04/src/components/Navbar/navbar.styled.js rename to mission/chapter04/mission_ch04/src/components/Navbar/navbar.style.jsx diff --git a/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.jsx b/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.jsx index 546272a..151183f 100644 --- a/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.jsx +++ b/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { SidebarContainer, SidebarLink } from "./sidebar.styled"; +import { SidebarContainer, SidebarLink } from "./sidebar.style"; import { IoSearch } from "react-icons/io5"; import { PiFilmSlateFill } from "react-icons/pi"; diff --git a/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.styled.js b/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.style.jsx similarity index 81% rename from mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.styled.js rename to mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.style.jsx index ac8a6ac..26da9f2 100644 --- a/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.styled.js +++ b/mission/chapter04/mission_ch04/src/components/Sidebar/sidebar.style.jsx @@ -2,11 +2,10 @@ import styled from "styled-components"; import { Link } from "react-router-dom"; export const SidebarContainer = styled.div` - position: fixed; + position: relative; left: 0; - top: 60px; width: 15%; - height: calc(100% - 60px); /* Navbar 높이를 제외한 남은 공간 */ + min-height: 100vh; /* Navbar 높이를 제외한 남은 공간 */ background-color: #1b1b1b; display: flex; flex-direction: column; diff --git a/mission/chapter04/mission_ch04/src/hooks/useCustomFetch.js b/mission/chapter04/mission_ch04/src/hooks/useCustomFetch.js index 8e53cc1..b97dcbc 100644 --- a/mission/chapter04/mission_ch04/src/hooks/useCustomFetch.js +++ b/mission/chapter04/mission_ch04/src/hooks/useCustomFetch.js @@ -5,12 +5,17 @@ const useCustomFetch = (url) =>{ const [data, setData] = useState([]); // 데이터 저장 const [isLoading, setIsLoading] = useState(false); // 데이터 로딩 여부 const [isError, setIsError] = useState(false); // 에러 발생 여부 - + useEffect(()=>{ // url이 바뀔 때마다 데이터 가져오기 const fetchData = async()=>{ // 데이터 가져오는 비동기 함수 setIsLoading(true); // 로딩 상태를 true로 변경하여 데이터가 가져오는 중임을 나타냄 try{ - const response = await axios.get(url); + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${import.meta.env.VITE_TMDB_ACCESS_TOKEN}` + } + }); + //console.log("API Response:", response.data); setData(response.data); // 데이터 요청 성공 시 데이터 저장 } catch (error){ // 데이터 요청 실패 시 에러 처리 diff --git a/mission/chapter04/mission_ch04/src/layout/root-layout.jsx b/mission/chapter04/mission_ch04/src/layout/root-layout.jsx index 1a85052..b38feb3 100644 --- a/mission/chapter04/mission_ch04/src/layout/root-layout.jsx +++ b/mission/chapter04/mission_ch04/src/layout/root-layout.jsx @@ -15,9 +15,9 @@ const RootLayout = () => { - - - + + + ); diff --git a/mission/chapter04/mission_ch04/src/layout/root-layout.styled.js b/mission/chapter04/mission_ch04/src/layout/root-layout.styled.js index 4005801..8dce4f9 100644 --- a/mission/chapter04/mission_ch04/src/layout/root-layout.styled.js +++ b/mission/chapter04/mission_ch04/src/layout/root-layout.styled.js @@ -6,5 +6,4 @@ export const LayoutContainer = styled.div` export const MainContent = styled.div` flex: 1; /* 사이드바를 제외한 나머지 공간을 차지 */ - margin-left: 15%; `; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.jsx b/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.jsx index 79882fa..9b87013 100644 --- a/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.jsx +++ b/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.jsx @@ -1,7 +1,7 @@ // 카테고리 목록을 출력하는 페이지 import React from "react"; import { Link } from "react-router-dom"; -import { CategoryContainer, CategoryTitle, CategoryCard, CategoryImage } from "./CategoryList.styled"; +import { CategoryContainer, CategoryHeaderName, CategoryTitle, CategoryCard, CategoryImage, CategoryBox } from "./CategoryList.style"; import image1 from '../../assets/sanrio1.png'; import image2 from '../../assets/sanrio2.png'; @@ -40,16 +40,18 @@ const CategoryList = () => { return ( <> - {categories.map((category) => ( // 카테고리 목록 순회 - {/*클릭 시 movies/:category 경로로 이동*/} - - - {category.title} - - - ))} + 카테고리 목록 + + {categories.map((category) => ( // 카테고리 목록 순회 + {/*클릭 시 movies/:id 경로로 이동*/} + + + {category.title} + + + ))} + ); diff --git a/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.styled.js b/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.style.jsx similarity index 57% rename from mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.styled.js rename to mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.style.jsx index 1504e6f..b5ccc9d 100644 --- a/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.styled.js +++ b/mission/chapter04/mission_ch04/src/pages/CategoryList/CategoryList.style.jsx @@ -1,45 +1,55 @@ import styled from "styled-components"; export const CategoryContainer = styled.div` - display: flex; - flex-direction: column; - align-items: center; padding: 20px; - background-color: #181818; + background-color: #000; min-height: 100vh; `; -export const CategoryTitle = styled.h1` +export const CategoryHeaderName = styled.div` color: #fff; - margin-bottom: 30px; - font-size: 2rem; + margin: 5px 10px; + font-size: 30px; + font-weight: bold; + text-align: left; +`; + +export const CategoryBox = styled.div` + display: flex; + flex-direction: row; + justifyContent: center; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; `; export const CategoryCard = styled.div` background-color: #282828; border-radius: 10px; overflow: hidden; - width: 280px; + width: 260px; height: 180px; cursor: pointer; + position: relative; /* 자식 요소 절대 위치 설정에 필요 */ transition: transform 0.3s ease; + margin: 0 10px; &:hover { transform: scale(1.05); } &:hover > div { - display: block; /* MovieCard에 커서가 올라가면 자식 div (MovieLabel) 화면에 표시 */ + display: block; /* 마우스가 올라가면 Label 표시 */ } `; export const CategoryImage = styled.img` width: 100%; height: 100%; - object-fit: cover; // 이미지가 지정된 영역을 꽉 채우도록 설정 + object-fit: cover; `; -export const CategoryLabel = styled.div` +export const CategoryTitle = styled.div` position: absolute; bottom: 10px; left: 10px; diff --git a/mission/chapter04/mission_ch04/src/pages/ID.jsx b/mission/chapter04/mission_ch04/src/pages/ID.jsx deleted file mode 100644 index 51425f5..0000000 --- a/mission/chapter04/mission_ch04/src/pages/ID.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useParams } from 'react-router-dom'; -import MovieDetail from './MovieDetail/MovieDetail'; -import MovieList from './MovieList/MovieList'; - -const ID = ()=>{ - const {categoryOrMovieId} = useParams(); - const isNumber = /^\d+$/.test(categoryOrMovieId); - return isNumber ? - : -} - -export default ID; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.jsx b/mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.jsx deleted file mode 100644 index e52070a..0000000 --- a/mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import useCustomFetch from "../../hooks/useCustomFetch"; -import MovieCard from "../../components/MovieCard/MovieCard"; - -const APIurl={ - "now-playing" : '/movie/now_playing?language=ko-KR&page=1', - "popular" : '/movie/popular?language=ko-KR&page=1', - "top-rated" : '/movie/top_rated?language=ko-KR&page=1', - "up-coming" :'/movie/upcoming?language=ko-KR&page=1' -} - -const MovieList = (url) => { - const { category } = url; - - const { data: movies, isLoading, isError } = useCustomFetch(APIurl[category]); - if(isLoading){ - return
로딩중 입니다...
; - } - if(isError){ - return
에러 발생
; - } - - return ( - <> -
- {movies.data?.results.map((movie) => ( - - - - ))} -
- - ) - -} - -export default MovieList; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/MovieDetail/MovieDetail.css b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieDetail/MovieDetail.css similarity index 100% rename from mission/chapter04/mission_ch04/src/pages/MovieDetail/MovieDetail.css rename to mission/chapter04/mission_ch04/src/pages/MoviePages/MovieDetail/MovieDetail.css diff --git a/mission/chapter04/mission_ch04/src/pages/MovieDetail/MovieDetail.jsx b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieDetail/MovieDetail.jsx similarity index 66% rename from mission/chapter04/mission_ch04/src/pages/MovieDetail/MovieDetail.jsx rename to mission/chapter04/mission_ch04/src/pages/MoviePages/MovieDetail/MovieDetail.jsx index 04ee7fc..12ae80d 100644 --- a/mission/chapter04/mission_ch04/src/pages/MovieDetail/MovieDetail.jsx +++ b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieDetail/MovieDetail.jsx @@ -3,10 +3,15 @@ import MovieDetailCard from "../../../components/MovieDetailCard/MovieDetailCard import useCustomFetch from "../../../hooks/useCustomFetch"; import './MovieDetail.css' -const MovieDetailPage = (props)=>{ +const MovieDetail = (props)=>{ const {movieId} = props; - const { data : detail, isDetailLoading, isDetailError} = useCustomFetch(`/movie/${movieId}?language=ko-KR'`); - const {data : movie, isCastLoading, isCastError} = useCustomFetch(`/movie/${movieId}/credits?language=ko-KR`); + + const { data : detail, isDetailLoading, isDetailError} = + useCustomFetch(`https://api.themoviedb.org/3/movie/${movieId}?language=ko-KR`); + + const { data : movie, isCastLoading, isCastError } = + useCustomFetch(`https://api.themoviedb.org/3/movie/${movieId}/credits?language=ko-KR`); + if(isDetailLoading || isCastLoading){ return
로딩중 입니다...
} @@ -21,7 +26,7 @@ const MovieDetailPage = (props)=>{

감독/출연

{movie.data?.cast.map((cast)=>( - + ))}
@@ -30,4 +35,4 @@ const MovieDetailPage = (props)=>{ ) } -export default MovieDetailPage; \ No newline at end of file +export default MovieDetail; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.jsx b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.jsx new file mode 100644 index 0000000..862669b --- /dev/null +++ b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import useCustomFetch from "../../../hooks/useCustomFetch"; +import {MovieListContainer} from "./MovieList.style"; +import MovieCard from "../../../components/MovieCard/MovieCard"; + +const APIurl={ + "now_playing" : 'https://api.themoviedb.org/3/movie/now_playing?language=ko-KR&page=1', + "popular" : 'https://api.themoviedb.org/3/movie/popular?language=ko-KR&page=1', + "top_rated" : 'https://api.themoviedb.org/3/movie/top_rated?language=ko-KR&page=1', + "upcoming" :'https://api.themoviedb.org/3/movie/upcoming?language=ko-KR&page=1' +} + +const MovieList = (props) => { + const { category } = props; + const { data: movies, isLoading, isError } = useCustomFetch(APIurl[category]); + //console.log("Fetched URL:", APIurl[category]); + if(isLoading){ + return
로딩중 입니다...
; + } + if(isError){ + return
에러 발생
; + } + + return ( + <> + + {movies.data?.results.map((movie) => ( + + + + ))} + + + ) + +} + +export default MovieList; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.css b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.style.jsx similarity index 54% rename from mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.css rename to mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.style.jsx index 0cce9f1..b591003 100644 --- a/mission/chapter04/mission_ch04/src/pages/MovieList/MovieList.css +++ b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieList/MovieList.style.jsx @@ -1,9 +1,10 @@ -.movie_poster_list{ +import styled from 'styled-components'; + +export const MovieListContainer = styled.div` display: flex; flex-wrap: wrap; width: 100%; gap: 15px; margin-left: 15px; margin-top: 15px; - -} \ No newline at end of file +`; \ No newline at end of file diff --git a/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieRoute.jsx b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieRoute.jsx new file mode 100644 index 0000000..6939701 --- /dev/null +++ b/mission/chapter04/mission_ch04/src/pages/MoviePages/MovieRoute.jsx @@ -0,0 +1,14 @@ +import { useParams } from 'react-router-dom'; +import MovieDetail from './MovieDetail/MovieDetail'; +import MovieList from './MovieList/MovieList'; + +const MovieRoute = ()=>{ + const {id} = useParams(); // 카테고리 또는 영화의 ID를 가져옴 + const isNumber = /^\d+$/.test(id); + // ^\d+$ 는 숫자만 포함되어 있는지 확인하는 정규 표현식 + // 모든 문자가 숫자일 때만 true 반환 + return isNumber ? + : +} + +export default MovieRoute; \ No newline at end of file