https://youtu.be/05uFo_-SGXU
| |
| import Header from "./component/Header" |
| import DayList from "./component/DayList"; |
| import Day from "./component/Day"; |
| import CreateWord from "./component/CreateWord"; |
| import CreateDay from "./component/CreateDay"; |
| import {BrowserRouter, Routes, Route } from 'react-router-dom'; |
| import EmptyPage from "./component/EmptyPage"; |
| |
| export default function App() { |
| return ( |
| <BrowserRouter> |
| <div className="App"> |
| <Header /> |
| <Routes> |
| <Route exact path = "/" element = {<DayList />} /> |
| <Route path = "/day/:day" element = {<Day />} /> |
| <Route path = "/create_word" element = {<CreateWord />} /> |
| <Route path = "/create_day" element = {<CreateDay />} /> |
| <Route path = "*" element = {<EmptyPage />} /> |
| </Routes> |
| </div> |
| </BrowserRouter> |
| ); |
| } |
**v6 기준 Router 형식이 위와 같이 변경되었습니다 :) Switch는 이제 사용할 수 없다!
| |
| import { useNavigate } from "react-router"; |
| import useFetch from "../hooks/useFetch"; |
| |
| export default function CreatDay() { |
| const days = useFetch("http://localhost:3001/days"); |
| const history = useNavigate(); |
| |
| |
| function addDay(e){ |
| fetch(`http://localhost:3001/days/`, { |
| method : 'POST', |
| headers : { |
| 'Content-Type' : 'application/json', |
| }, |
| body : JSON.stringify({ |
| day : days.length + 1 |
| }), |
| }).then(res => { |
| if(res.ok){ |
| alert("생성 완료 >_<!"); |
| |
| history(`/`); |
| } |
| }) |
| } |
| |
| return ( |
| <div> |
| <h3>현재 일수 : {days.length}일</h3> |
| <button onClick={addDay}>Day 추가</button> |
| </div> |
| ); |
| } |
**v6 기준 useHistory가 아닌 useNavigate를 사용합니다! history.push가 아닌 history만 사용!
| |
| import useFetch from "../hooks/useFetch"; |
| import { useRef , useState} from "react"; |
| import { useNavigate } from "react-router-dom"; |
| |
| export default function CreateWord() { |
| const days = useFetch("http://localhost:3001/days"); |
| |
| const history = useNavigate(); |
| |
| const [isLoading, setIsLoading] = useState(false); |
| |
| |
| function onSubmit(e){ |
| e.preventDefault(); |
| |
| if(!isLoading) { |
| setIsLoading(true); |
| |
| |
| console.log(engRef.current.value); |
| console.log(korRef.current.value); |
| console.log(dayRef.current.value); |
| |
| fetch(`http://localhost:3001/words/`, { |
| method : 'POST', |
| headers : { |
| 'Content-Type' : 'application/json', |
| }, |
| body : JSON.stringify({ |
| eng : engRef.current.value, |
| kor : korRef.current.value, |
| day : dayRef.current.value, |
| isDone : false |
| }), |
| }).then(res => { |
| if(res.ok){ |
| alert("생성 완료 >_<!"); |
| |
| history(`/day/${dayRef.current.value}`); |
| setIsLoading(false); |
| } |
| }) |
| } |
| } |
| |
| |
| const engRef = useRef(null); |
| const korRef = useRef(null); |
| const dayRef = useRef(null); |
| |
| return ( |
| <form onSubmit={onSubmit}> |
| <div className="input_area"> |
| <label>Eng</label> |
| <input type = "text" placeholder="computer" ref={engRef}/> |
| </div> |
| <div className="input_area"> |
| <label>Kor</label> |
| <input type="text" placeholder="컴퓨터" ref={korRef}/> |
| </div> |
| <div className="input_area"> |
| <label>Day</label> |
| <select ref={dayRef}> |
| {days.map(day => ( |
| <option key = {day.id} value = {day.day}> |
| {day.day} |
| </option> |
| ))} |
| </select> |
| </div> |
| <button |
| style = {{ |
| opacity : isLoading ? 0.3 : 1, |
| }}>{isLoading ? "Saving..." : "저장"}</button> |
| </form> |
| ) |
| } |
| |
| import { useParams } from "react-router-dom"; |
| import Word from "./Word"; |
| import { useEffect, useState } from "react"; |
| import useFetch from "../hooks/useFetch"; |
| |
| export default function Day() { |
| |
| const {day} = useParams(); |
| const words = useFetch(`http://localhost:3001/words?day=${day}`); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| return ( |
| <> |
| <h2>Day {day}</h2> |
| {words.length === 0 && <span>Loading...</span>} |
| <table> |
| <tbody> |
| {words.map(word => ( |
| <Word word = {word} key = {word.id} /> |
| ))} |
| </tbody> |
| </table> |
| </> |
| ); |
| } |
| |
| import { useState , useEffect } from 'react'; |
| import { Link } from "react-router-dom"; |
| import useFetch from '../hooks/useFetch'; |
| |
| export default function DayList() { |
| |
| const days = useFetch('http://localhost:3001/days') |
| |
| if(days.length === 0){ |
| return <span>Loading...</span> |
| } |
| |
| return ( |
| <> |
| <ul className='list_day'> |
| {days.map(day => ( |
| <li key= {day.id}> |
| <Link to = {`/day/${day.day}`}> |
| Day {day.day} |
| </Link> |
| </li> |
| ))} |
| </ul> |
| </> |
| ) |
| } |
api에서 리스트를 가져와서 바꿔주는 방식, 데이터가 바뀌면 자동으로 렌더링됨
| const [days, setDays] = useState([]); |
| |
| |
| |
| useEffect(() => { |
| |
| fetch('http://localhost:3001/days') |
| |
| |
| .then(res => { |
| return res.json(); |
| }) |
| |
| .then(data => { |
| setDays(data); |
| }); |
| console.log("Count change"); |
| |
| |
| |
| }, []); |
| |
| import { Link } from "react-router-dom"; |
| |
| export default function EmptyPage() { |
| |
| return ( |
| <div> |
| <h2>잘못된 접근입니다.</h2> |
| <Link to = "/">돌아가기</Link> |
| </div> |
| ) |
| } |
| |
| import {Link} from 'react-router-dom'; |
| |
| export default function Header() { |
| return ( |
| <div className = "header"> |
| <h1> |
| <Link to ="/">토익 영단어(고급)</Link> |
| </h1> |
| <div className="menu"> |
| <Link to = "/create_word" className='link'> |
| 단어 추가 |
| </Link> |
| <Link to = "/create_day" className='link'> |
| Day 추가 |
| </Link> |
| </div> |
| </div> |
| ) |
| } |
| |
| import { useState } from "react"; |
| |
| export default function Word({word: w}) { |
| const [word, setWord] = useState(w); |
| const [isShow, setIsShow ] = useState(false); |
| const [isDone, setIsDone ] = useState(word.isDone); |
| |
| function toggleShow() { |
| setIsShow(!isShow) |
| } |
| |
| function toggleDone() { |
| |
| fetch(`http://localhost:3001/words/${word.id}`, { |
| method : 'PUT', |
| headers : { |
| 'Content-Type' : 'application/json', |
| }, |
| body : JSON.stringify({ |
| ...word, |
| isDone : !isDone |
| }), |
| }).then(res => { |
| if(res.ok){ |
| setIsDone(!isDone); |
| } |
| }) |
| } |
| |
| function del(){ |
| if(window.confirm('삭제 하시겠습니까?')) { |
| fetch(`http://localhost:3001/words/${word.id}`, { |
| method : 'DELETE', |
| }).then (res => { |
| |
| if(res.ok) { |
| setWord({id:0}); |
| } |
| }) |
| } |
| } |
| |
| if(word.id === 0) { |
| return null; |
| } |
| |
| return ( |
| <tr className={isDone ? "off" : ""}> |
| <td><input type="checkbox" checked = {isDone} onChange = {toggleDone}/></td> |
| <td>{word.eng}</td> |
| {/* // isShow일 때는 */} |
| <td>{isShow && word.kor}</td> |
| <td> |
| <button onClick = {toggleShow}> 뜻 {isShow ? "숨기기" : "보기" }</button> |
| <button className = "btn_del" onClick={del}>삭제</button> |
| </td> |
| </tr> |
| ) |
| } |
| |
| { |
| "days": [ |
| { |
| "id": 1, |
| "day": 1 |
| }, |
| { |
| "id": 2, |
| "day": 2 |
| }, |
| { |
| "id": 3, |
| "day": 3 |
| }, |
| { |
| "id": 4, |
| "day": 4 |
| }, |
| { |
| "day": 5, |
| "id": 5 |
| } |
| ], |
| "words": [ |
| { |
| "id": 1, |
| "day": 1, |
| "eng": "book", |
| "kor": "책", |
| "isDone": false |
| }, |
| { |
| "id": 3, |
| "day": 2, |
| "eng": "car", |
| "kor": "자동차", |
| "isDone": false |
| }, |
| { |
| "id": 4, |
| "day": 2, |
| "eng": "pen", |
| "kor": "펜", |
| "isDone": false |
| }, |
| { |
| "id": 5, |
| "day": 3, |
| "eng": "school", |
| "kor": "학교", |
| "isDone": false |
| }, |
| { |
| "id": 6, |
| "day": 3, |
| "eng": "pencil", |
| "kor": "연필", |
| "isDone": false |
| }, |
| { |
| "day": "3", |
| "eng": "window", |
| "kor": "창문", |
| "isDone": false, |
| "id": 7 |
| }, |
| { |
| "day": "3", |
| "eng": "house", |
| "kor": "집", |
| "isDone": false, |
| "id": 8 |
| }, |
| { |
| "day": "2", |
| "eng": "mouse", |
| "kor": "쥐", |
| "isDone": false, |
| "id": 9 |
| }, |
| { |
| "day": "4", |
| "eng": "monkey", |
| "kor": "원숭이", |
| "isDone": false, |
| "id": 10 |
| }, |
| { |
| "day": "4", |
| "eng": "apple", |
| "kor": "사과", |
| "isDone": false, |
| "id": 11 |
| }, |
| { |
| "eng": "dorothy", |
| "kor": "도로시", |
| "day": "1", |
| "isDone": false, |
| "id": 12 |
| }, |
| { |
| "eng": "window", |
| "kor": "창문", |
| "day": "3", |
| "isDone": false, |
| "id": 13 |
| }, |
| { |
| "eng": "test", |
| "kor": "시험", |
| "day": "1", |
| "isDone": false, |
| "id": 14 |
| }, |
| { |
| "eng": "", |
| "kor": "", |
| "day": "", |
| "isDone": false, |
| "id": 15 |
| }, |
| { |
| "eng": "jiwon", |
| "kor": "양지", |
| "day": "1", |
| "isDone": false, |
| "id": 16 |
| } |
| ] |
| } |
| |
| import { useEffect, useState } from "react"; |
| |
| export default function useFetch(url) { |
| const [data, setData] = useState([]); |
| |
| useEffect(() => { |
| fetch(url) |
| .then(res => { |
| return res.json(); |
| }) |
| .then(data => { |
| setData(data); |
| }); |
| }, [url]); |
| |
| return data; |
| } |
| |
| 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; |
| font-size: 20px; |
| } |
| |
| ol, |
| ul { |
| margin: 0; |
| padding: 0; |
| list-style: none; |
| } |
| |
| code { |
| font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", |
| monospace; |
| } |
| |
| a { |
| text-decoration: none; |
| color: #333; |
| } |
| |
| .App { |
| width: 800px; |
| margin: 0 auto; |
| } |
| |
| .header { |
| position: relative; |
| } |
| |
| .header .menu { |
| position: absolute; |
| top: 10px; |
| right: 0; |
| } |
| |
| .header .link { |
| border: 1px solid #333; |
| padding: 10px; |
| margin-left: 10px; |
| background-color: #efefef; |
| font-weight: bold; |
| border-radius: 4px; |
| } |
| |
| .list_day { |
| display: flex; |
| flex-wrap: wrap; |
| } |
| |
| .list_day li { |
| flex: 20% 0 0; |
| box-sizing: border-box; |
| padding: 10px; |
| } |
| |
| .list_day a { |
| display: block; |
| padding: 20px 0; |
| font-weight: bold; |
| color: #fff; |
| text-align: center; |
| border-radius: 10px; |
| background-color: dodgerblue; |
| } |
| |
| table { |
| border-collapse: collapse; |
| width: 100%; |
| } |
| table td { |
| width: 25%; |
| height: 70px; |
| border: 1px solid #ccc; |
| text-align: center; |
| font-size: 26px; |
| } |
| |
| table td:first-child { |
| width: 10%; |
| } |
| |
| .off td { |
| background: #eee; |
| color: #ccc; |
| } |
| |
| .btn_del { |
| margin-left: 10px; |
| color: #fff; |
| background-color: firebrick; |
| } |
| |
| button { |
| padding: 10px; |
| font-weight: bold; |
| font-size: 18px; |
| cursor: pointer; |
| border: 0 none; |
| border-radius: 6px; |
| padding: 10px 20px; |
| color: #fff; |
| background-color: dodgerblue; |
| } |
| |
| .input_area { |
| margin-bottom: 10px; |
| } |
| |
| .input_area label { |
| display: block; |
| margin-bottom: 10px; |
| } |
| |
| .input_area input { |
| width: 400px; |
| height: 40px; |
| font-size: 20px; |
| padding: 0 10px; |
| } |
| |
| .input_area select { |
| width: 400px; |
| height: 40px; |
| font-size: 20px; |
| } |
| |
| import React from 'react'; |
| import ReactDOM from 'react-dom/client'; |
| import './index.css'; |
| import App from './App'; |
| |
| const root = ReactDOM.createRoot(document.getElementById('root')); |
| root.render( |
| <React.StrictMode> |
| <App /> |
| </React.StrictMode> |
| ); |