Mini_Project_02

정상희·2025년 4월 24일

코딩공부

목록 보기
44/60
post-thumbnail

요구 사항

  1. Todo 생성 / 조회 / 수정 / 삭제 (CRUD) 기능을 구현하세요.
  2. 현재 시간 표시, 타이머, 스톱워치 중 하나 이상의 기능을 구현하세요.
  3. 랜덤 명언을 표시할 수 있는 컴포넌트를 만드세요.
  4. useState, useEffect, useRef를 각각 한 번 이상 사용하세요.
  5. 자유롭게 적용해보고 싶은 CSS를 작성해보세요.
  6. json-server를 사용해 Todo 정보를 파일로 저장해 보세요.
  7. Custom Hook을 만들고 사용해 보세요.

요구사항은 이러하다! 최대한 요구사항에 맞춰 만들어 봄.

1. Todo 생성 / 조회 / 수정 / 삭제 (CRUD) 기능을 구현하세요. useState 사용

todoList. jsx

import { useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo, onUpdate, onDelete }) => { 
    const [search, setSearch] = useState(""); 
    const onChangeSearch = (e) => { 
        setSearch(e.target.value);
    };
    const getSearchResult = () => {
        return search === ""
        ? todo
        : todo.filter((it) =>
            it.content.toLowerCase().includes(search.toLowerCase())
        );
    }

    return (
        <div className="TodoList">
            <h4>Todo List 🌱</h4>
            <input 
                value={search}
                onChange={onChangeSearch} 
                className="searchbar" 
                placeholder="검색어를 입력하세요"/>
            <div className="list_wrapper">
                {getSearchResult().map((it) => (
                    <TodoItem key={it.id} {...it} onUpdate={onUpdate} onDelete={onDelete} />
                ))}
            </div>
        </div>
    );
};

export default TodoList;

todoItem. jsx

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => {
    const onChangeCheckbox = () => {
        onUpdate(id);
    };
    const onClickDelete = () => {
        onDelete(id);
    }

    return (
        <div className="TodoItem">
            <div className="checkbox_col">
                <input 
                    onChange={onChangeCheckbox} 
                    checked={isDone} 
                    type="checkbox" 
                />
            </div>
            <div className="title_col">{content}</div>
            <div className="date_col">
                {new Date(createdDate).toLocaleDateString()}
            </div>
            <div className="btn_col">
                <button onClick={onClickDelete}></button>
            </div>
        </div>
    );
};

export default TodoItem;

todoEditor. jsx : useRef 사용

import { useState, useRef } from "react";
import "./TodoEditor.css";

const TodoEditor = ({ onCreate }) => {
    const [content, setcontent] = useState("");
    const inputRef = useRef();
    const onChangeContent = (event) => {
        setcontent(event.target.value);
    };
    const onSubmit = () => {
        if (!content) {
            inputRef.current.focus();
            return;
        }
        onCreate(content);
        setcontent('');
    };
    const onKeyDown = (event) => {
        if (event.keyCode === 13) {
            onSubmit();
        }
    };

    return (
        <div className="TodoEditor">
            <h4>새로운 Todo 작성하기 🔏 </h4>
            <div className="editor_wrapper">
                <input
                    ref={inputRef}
                    value={content}
                    onChange={onChangeContent}
                    onKeyDown={onKeyDown}
                    placeholder="새로운 Todo..."
                />
                <button onClick={onSubmit}>추가</button>
            </div>
        </div>
    );
};

export default TodoEditor;

2. 현재 시간 표시, 타이머, 스톱워치 중 하나 이상의 기능을 구현하세요.

header. jsx

import "./Header.css";

const Header = () => {
    return (
        <div className="Header">
            <h3>오늘은 📅</h3>
            <h1>{new Date().toDateString()}</h1>
        </div>
    );
};

export default Header;

3. 랜덤 명언을 표시할 수 있는 컴포넌트를 만드세요.

Quote. jsx : useEffect 사용

import React, { useState, useEffect, useCallback } from 'react'; // useCallback 추가
import axios from 'axios';
import './Quote.css';

function Quote() {
  const [quote, setQuote] = useState({ content: '', author: '' });
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  // --- 명언 가져오는 함수 분리 ---
  // useCallback으로 감싸서 불필요한 재생성을 방지 (선택 사항이지만 권장)
  const fetchQuote = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await axios.get('https://korean-advice-open-api.vercel.app/api/advice');
      setQuote({
        content: response.data.message,
        author: response.data.author,
      });
    } catch (err) {
      console.error("명언 가져오기 실패:", err);
      setError("명언을 불러오는 데 실패했습니다.");
      // 에러 발생 시 이전 명언이 남아있지 않도록 초기화 (선택 사항)
      // setQuote({ content: '', author: '' });
    } finally {
      setIsLoading(false);
    }
  }, []); // 의존성 배열이 비어있으므로 컴포넌트 마운트 시 한 번만 생성됨
  // ------------------------------

  // 컴포넌트 마운트 시 명언 가져오기
  useEffect(() => {
    fetchQuote(); // 분리된 함수 호출
  }, [fetchQuote]); // useEffect의 의존성 배열에 fetchQuote 추가

  // 로딩 상태 표시
  if (isLoading) {
    // 로딩 중일 때는 버튼을 숨기거나 비활성화할 수 있습니다.
    return (
        <div className="quote-container">
             명언 로딩 중...
             {/* 로딩 중 버튼 예시 (비활성화) */}
             {/* <button disabled style={{marginTop: '10px'}}>새 명언</button> */}
        </div>
    );
  }

  // 에러 상태 표시
  if (error) {
    return (
      <div className="quote-container error">
        오류: {error}
        {/* 에러 발생 시에도 재시도 버튼을 보여줄 수 있음 */}
        <button onClick={fetchQuote} style={{ marginTop: '10px' }}>다시 시도</button>
      </div>
    );
  }

  // 명언 표시
  return (
    <div className="quote-container">
      <blockquote className="quote-text">
        "{quote.content}"
      </blockquote>
      {quote.author && quote.author !== "알 수 없음" && (
          <p className="quote-author">- {quote.author} -</p>
      )}

      {/* --- 새 명언 버튼 추가 --- */}
      <button className='quoteBtn' onClick={fetchQuote}>
        새 명언 보기
      </button>
      {/* ----------------------- */}
    </div>
  );
}

export default Quote;
profile
UI/UX디자이너의 코딩 공부

0개의 댓글