From 0a5f8068fcf39ac7914dffa05fcb3149bf144c75 Mon Sep 17 00:00:00 2001 From: 05ovo2e Date: Sun, 23 Jun 2024 22:18:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Round2]=20React=20=EC=9B=B9=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dicegame/App.js" | 12 +++ .../dicegame/Dice.js" | 0 .../dicegame/index.js" | 10 ++ .../hello_react/App.css" | 38 ++++++++ .../hello_react/App.js" | 26 ++++++ .../hello_react/index.css" | 13 +++ .../hello_react/index.js" | 17 ++++ .../rock-scissor-paper/App.js" | 76 ++++++++++++++++ .../rock-scissor-paper/Button.js" | 8 ++ .../rock-scissor-paper/HandButton.css" | 27 ++++++ .../rock-scissor-paper/HandButton.js" | 14 +++ .../rock-scissor-paper/HandIcon.js" | 17 ++++ .../rock-scissor-paper/index.js" | 4 + .../rock-scissor-paper/utils.js" | 22 +++++ .../foodit/App.js" | 91 +++++++++++++++++++ .../foodit/FoodList.js" | 30 ++++++ .../foodit/api.js" | 14 +++ .../foodit/index.html" | 43 +++++++++ .../foodit/index.js" | 4 + .../moviepedia/App.js" | 81 +++++++++++++++++ .../moviepedia/ReviewForm.css" | 9 ++ .../moviepedia/ReviewForm.js" | 31 +++++++ .../moviepedia/ReviewList.css" | 12 +++ .../moviepedia/ReviewList.js" | 56 ++++++++++++ .../moviepedia/api.js" | 15 +++ .../moviepedia/index.html" | 10 ++ .../moviepedia/index.js" | 4 + 27 files changed, 684 insertions(+) create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/App.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/Dice.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/index.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.css" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.css" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/App.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/Button.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.css" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandIcon.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/index.js" create mode 100644 "05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/utils.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/App.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/FoodList.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/api.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.html" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/App.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.css" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewList.css" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewList.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/api.js" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/index.html" create mode 100644 "05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/index.js" diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/App.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/App.js" new file mode 100644 index 00000000..a1a20646 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/App.js" @@ -0,0 +1,12 @@ +import Dice from './Dice'; + +function App() { + return ( +
+ +
+ ); + +} + +export default App; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/Dice.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/Dice.js" new file mode 100644 index 00000000..e69de29b diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/index.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/index.js" new file mode 100644 index 00000000..e3ca7986 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/dicegame/index.js" @@ -0,0 +1,10 @@ + +import ReactDOM from 'react-dom/client'; +import App from './App'; + + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + , document.getElementById('root') +); + diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.css" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.css" new file mode 100644 index 00000000..74b5e053 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.css" @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.js" new file mode 100644 index 00000000..19acd9a8 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/App.js" @@ -0,0 +1,26 @@ +import logo from './logo.svg'; +import './App.css'; + +function App() { + return ( +
+
+

안녕 리액트!

+ logo +

+ Edit src/App.js and save to reload. +

+ + Learn React + +
+
+ ); +} + +export default App; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.css" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.css" new file mode 100644 index 00000000..ec2585e8 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.css" @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.js" new file mode 100644 index 00000000..d563c0fb --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/hello_react/index.js" @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/App.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/App.js" new file mode 100644 index 00000000..146d01b5 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/App.js" @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import Button from './Button'; +import HandButton from './HandButton'; +import HandIcon from './HandIcon'; +import { compareHand, generateRandomHand } from './utils'; + +const INITIAL_VALUE = 'rock'; + +function getResult(me, other) { + const comparison = compareHand(me, other); + if (comparison > 0) return '승리'; + if (comparison < 0) return '패배'; + return '무승부'; +} + +function App() { + const [hand, setHand] = useState(INITIAL_VALUE); + const [otherHand, setOtherHand] = useState(INITIAL_VALUE); + const [gameHistory, setGameHistory] = useState([]); + const [score, setScore] = useState(0); + const [otherScore, setOtherScore] = useState(0); + const [bet, setBet] = useState(1); + + const handleButtonClick = (nextHand) => { + const nextOtherHand = generateRandomHand(); + const nextHistoryItem = getResult(nextHand, nextOtherHand); + const comparison = compareHand(nextHand, nextOtherHand); + setHand(nextHand); + setOtherHand(nextOtherHand); + setGameHistory([...gameHistory, nextHistoryItem]); + if (comparison > 0) setScore(score + bet); + if (comparison < 0) setOtherScore(otherScore + bet); + }; + + const handleClearClick = () => { + setHand(INITIAL_VALUE); + setOtherHand(INITIAL_VALUE); + setGameHistory([]); + setScore(0); + setOtherScore(0); + setBet(1); + }; + + const handleBetChange = (e) => { + let num = Number(e.target.value); + if (num > 9) num %= 10; + if (num < 1) num = 1; + num = Math.floor(num); + setBet(num); + }; + + return ( +
+ +
+ {score} : {otherScore} +
+
+ + VS + +
+
+ +
+

승부 기록: {gameHistory.join(', ')}

+
+ + + +
+
+ ); +} + +export default App; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/Button.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/Button.js" new file mode 100644 index 00000000..a2796459 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/Button.js" @@ -0,0 +1,8 @@ + + +function Button({ children, onClick }) { + + return ; +} + +export default Button; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.css" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.css" new file mode 100644 index 00000000..809a0b1c --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.css" @@ -0,0 +1,27 @@ +.HandButton { + width: 166px; + height: 166px; + border: none; + outline: none; + text-align: center; + cursor: pointer; + background-color: transparent; + background-image: url('./assets/purple.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.HandButton .HandButton-icon { + opacity: 0.45; + width: 55px; + height: 55px; +} + +.HandButton:hover { + background-image: url('./assets/yellow.svg'); +} + +.HandButton:hover .HandButton-icon { + opacity: 0.87; +} diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.js" new file mode 100644 index 00000000..b522c935 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandButton.js" @@ -0,0 +1,14 @@ +import HandIcon from './HandIcon'; +import './HandButton.css'; + +// CSS 파일로 스타일을 적용해 주세요 +function HandButton({ value, onClick }) { + const handleClick = () => onClick(value); + return ( + + ); +} + +export default HandButton; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandIcon.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandIcon.js" new file mode 100644 index 00000000..cba3552d --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/HandIcon.js" @@ -0,0 +1,17 @@ +import rockImg from './assets/rock.svg'; +import scissorImg from './assets/scissor.svg'; +import paperImg from './assets/paper.svg'; + +const IMAGES = { + rock: rockImg, + scissor: scissorImg, + paper: paperImg, +}; + +// className prop을 추가하고, img 태그에 적용해주세요 +function HandIcon({ className, value }) { + const src = IMAGES[value]; + return {value}; +} + +export default HandIcon; diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/index.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/index.js" new file mode 100644 index 00000000..606d7ac4 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/index.js" @@ -0,0 +1,4 @@ +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git "a/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/utils.js" "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/utils.js" new file mode 100644 index 00000000..ee002c31 --- /dev/null +++ "b/05ovo2e/Round2/React \354\233\271 \352\260\234\353\260\234 \354\213\234\354\236\221\355\225\230\352\270\260/rock-scissor-paper/utils.js" @@ -0,0 +1,22 @@ +const HANDS = ['rock', 'scissor', 'paper']; + +const WINS = { + rock: 'scissor', + scissor: 'paper', + paper: 'rock', +}; + +export function compareHand(a, b) { + if (WINS[a] === b) return 1; + if (WINS[b] === a) return -1; + return 0; +} + +function random(n) { + return Math.floor(Math.random() * n); +} + +export function generateRandomHand() { + const idx = random(HANDS.length); + return HANDS[idx]; +} diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/App.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/App.js" new file mode 100644 index 00000000..0004b973 --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/App.js" @@ -0,0 +1,91 @@ +import FoodList from "./FoodList"; +import { useEffect, useState } from "react"; +import { getFoods } from "../api"; + +function App() { + // 칼로리 높은 순으로 정렬 + const [order, setOrder] = useState("createdAt"); + const [items, setItems] = useState([]); + const [cursor, setCursor] = useState(null); + + //isLoading : 로딩 상태, loadingError: 에러 객체를 값으로 함 + const [isLoading, setIsLoading] = useState(false); + const [loadingError, setLoadingError] = useState(null); + + // 검색 기능 + const [search, setSearch] = useState(""); + + const sortedItems = items.sort((a, b) => b[order] - a[order]); + + const handleNewestClick = () => setOrder("createdAt"); + const handleCalorieClick = () => setOrder("calorie"); + + const handleDelete = (id) => { + const nextItems = items.filter((item) => item.id !== id); + setItems(nextItems); + }; + + const handleLoad = async (options) => { + let result; + try { + setLoadingError(null); + setIsLoading(true); + result = await getFoods(options); + } catch (error) { + setLoadingError(error); + return; + } finally { + setIsLoading(false); + } + + const { + foods, + paging: { nextCursor }, + } = result; + + if (!options.cursor) { + setItems(foods); + } else { + setItems((prevItems) => [...prevItems, ...foods]); + } + setCursor(nextCursor); + }; + + const handleLoadMore = () => { + handleLoad({ order, cursor, search }); + }; + + const handleSearchSubmit = (e) => { + e.preventDefault(); + setSearch(e.target["search"].value); + }; + + useEffect(() => { + handleLoad({ + order, + search, + }); + }, [order, search]); + + return ( +
+
+ + +
+ + +
+
+ + {cursor && ( + + )} + {loadingError?.message && {loadingError.message}} +
+ ); +} + +export default App; diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/FoodList.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/FoodList.js" new file mode 100644 index 00000000..f965a68e --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/FoodList.js" @@ -0,0 +1,30 @@ +function FoodListItem({ item, onDelete }) { + const { imgUrl, title, calorie, content } = item; + const handleDeleteClick = () => { + onDelete(item.id); + }; + + return ( +
+ {title} +
{title}
+
{calorie}
+
{content}
+ +
+ ); +} + +function FoodList({ items, onDelete }) { + return ( +
    + {items.map((item) => ( +
  • + +
  • + ))} +
+ ); +} + +export default FoodList; diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/api.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/api.js" new file mode 100644 index 00000000..aa4df299 --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/api.js" @@ -0,0 +1,14 @@ +export async function getFoods({ + order = "", + cursor = "", + limit = 10, + search = "", +}) { + const query = `order=${order}&cursor=${cursor}&limit=${limit}&search=${search}`; + const response = await fetch(`https://learn.codeit.kr/api/foods?${query}`); + if (!response.ok) { + throw new Error("데이터를 불러오는데 실패했습니다"); + } + const body = await response.json(); + return body; +} diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.html" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.html" new file mode 100644 index 00000000..aa069f27 --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.html" @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.js" new file mode 100644 index 00000000..463687cd --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/foodit/index.js" @@ -0,0 +1,4 @@ +import ReactDOM from 'react-dom'; +import App from './components/App'; + +ReactDOM.render(, document.getElementById('root')); diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/App.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/App.js" new file mode 100644 index 00000000..37f0478a --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/App.js" @@ -0,0 +1,81 @@ +import ReviewList from "./ReviewList"; +import { useEffect, useState } from "react"; +import { getReviews } from "../api"; +import ReviewForm from "./ReviewForm"; + +const LIMIT = 6; + +// 최상위 컴포넌트 +function App() { + // 별점 높은 순으로 정렬 + const [order, setOrder] = useState("createdAt"); + const [offset, setOffset] = useState(0); + const [hasNext, setHasNext] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [loadingError, setLoadingError] = useState(null); + const [items, setItems] = useState([]); + const sortedItems = items.sort((a, b) => b[order] - a[order]); + + const handleNewestClick = () => setOrder("createdAt"); + const handleBestClick = () => setOrder("rating"); + + const handleDelete = (id) => { + const nextItems = items.filter((item) => item.id !== id); + setItems(nextItems); + }; + + const handleLoad = async (options) => { + // 네트워크 로딩 처리 try-catch-finally와 isLoading 값 이용 + let result; + try { + setLoadingError(null); + setIsLoading(true); + result = await getReviews(options); + } catch (error) { + setLoadingError(error); + return; + } finally { + setIsLoading(false); + } + const { paging, reviews } = result; + + if (options.offset === 0) { + setItems(reviews); + } else { + // 콜백함수 prevItems 사용으로 삭제한 데이터가 다시 set되는 것 방지 + setItems((prevItems) => [...prevItems, ...reviews]); + } + setOffset(options.offset + options.limit); + setHasNext(paging.hasNext); + }; + + const handleLoadMore = async () => { + await handleLoad({ order, offset, limit: LIMIT }); + }; + + // 실행할 콜백 함수와 빈 배열을 넘겨 주면 + // 리액트는 콜백 함수를 맨 처음 렌더링할 때만 실행함 (무한 루프x) + useEffect(() => { + handleLoad({ order, offset: 0, limit: LIMIT }); + // order 버튼 클릭마다 state가 바뀌어서 재렌더링이 일어남 + // 정렬된 데이터 서버 요청에 사용 + }, [order]); + + return ( +
+
+ + +
+ + {hasNext && ( + + )} + {loadingError?.message && {loadingError.message}} +
+ ); +} + +export default App; diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.css" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.css" new file mode 100644 index 00000000..b6bf3951 --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.css" @@ -0,0 +1,9 @@ +.ReviewForm { + display: flex; + flex-direction: column; + padding: 10px 0; +} + +.ReviewForm > :not(:last-child) { + margin-bottom: 20px; +} diff --git "a/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.js" "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.js" new file mode 100644 index 00000000..5e266886 --- /dev/null +++ "b/05ovo2e/Round2/React\353\241\234 \353\215\260\354\235\264\355\204\260 \353\213\244\353\243\250\352\270\260/moviepedia/ReviewForm.js" @@ -0,0 +1,31 @@ +import { useState } from "react"; +import "./ReviewForm.css"; + +function ReviewForm() { + const [title, setTitle] = useState(""); + const [rating, setRating] = useState(0); + const [content, setContent] = useState(""); + + const handleTitleChange = (e) => { + setTitle(e.target.value); + }; + + const handleRatingChange = (e) => { + const nextRating = Number(e.target.value); + setRating(nextRating); + }; + + const handleContentChange = (e) => { + setContent(e.target.value); + }; + + return ( +
+ + +