React - 삭제기능 구현, 타이틀 수정, 린캔버스 상세 UI

김명원·2025년 1월 9일
0

learnReact

목록 보기
19/26

삭제기능 구현 🗑️✨

이번 포스트에서는 삭제 기능 구현에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.

실습 🛠️


src/api/canvas.js 수정 📄

deleteCanvas 함수를 추가하여 캔버스 데이터를 삭제하는 기능을 구현했습니다.

import { canvases } from './http';
import { v4 as uuidv4 } from 'uuid';
import dayjs from 'dayjs';

export function getCanvases(params) {
  const payload = Object.assign(
    {
      _sort: 'lastModified',
      _order: 'desc',
    },
    params,
  );
  return canvases.get('/', { params: payload });
}

export function createCanvas() {
  const newCanvas = {
    title: uuidv4().substring(0, 4) + '_새로운 린 캔버스',
    lastModified: dayjs().format('YYYY-MM-DD HH:mm:ss'),
    category: '신규',
  };
  return canvases.post('/', newCanvas);
}

export async function deleteCanvas(id) {
  await canvases.delete(`/${id}`);
}

코드 설명:

  • deleteCanvas 함수는 전달된 id를 사용하여 해당 캔버스를 삭제합니다.
  • RESTful API의 DELETE 메소드를 활용하여 서버에서 데이터를 제거합니다.

추가 설명:
삭제 기능을 구현함으로써 사용자가 더 이상 필요하지 않은 캔버스를 손쉽게 제거할 수 있게 되었습니다. 이는 데이터 관리의 효율성을 높이고, 사용자 경험을 향상시키는 중요한 기능입니다.

src/pages/Home.jsx 수정 🏠

Home 컴포넌트에 삭제 기능을 추가하여 사용자 인터페이스에서 캔버스를 삭제할 수 있도록 했습니다.

import { useEffect, useState } from 'react';
+import { createCanvas, deleteCanvas, getCanvases } from '../api/canvas';
 
import CanvasList from '../components/CanvasList';
import SearchBar from '../components/SearchBar';
// ...생략...
     fetchData({ title_like: searchText });
   }, [searchText]);

+  const handleDeleteItem = async id => {
+    if (confirm('삭제 하시겠습니까?') === false) {
+      return;
+    }
+    try {
+      await deleteCanvas(id);
+      fetchData({ title_like: searchText });
+    } catch (err) {
+      alert(err.message);
+    }
   };

   const [isLoadingCreate, setIsLoadingCreate] = useState(false);
// ...생략...

코드 설명:

  • handleDeleteItem 함수는 삭제를 원하는 캔버스의 id를 인자로 받아 삭제를 수행합니다.
  • 사용자에게 삭제 확인 메시지를 표시하여 의도치 않은 삭제를 방지합니다.
  • 삭제가 성공하면 fetchData를 호출하여 최신 데이터를 다시 불러옵니다.
  • 오류가 발생할 경우, 사용자에게 오류 메시지를 알립니다.

추가 설명:
삭제 기능을 추가함으로써 사용자는 불필요한 캔버스를 쉽게 제거할 수 있게 되었습니다. 또한, 삭제 후 데이터를 다시 불러오는 로직을 통해 UI가 항상 최신 상태를 유지하도록 했습니다.

전체 수정 사항 요약 📋

  • src/api/canvas.jsdeleteCanvas 함수를 추가하여 서버에서 캔버스를 삭제할 수 있는 기능을 구현했습니다.
  • src/pages/Home.jsxhandleDeleteItem 함수를 추가하여 사용자 인터페이스에서 캔버스를 삭제할 수 있도록 했습니다.
  • 사용자 경험을 향상시키기 위해 삭제 시 확인 메시지를 추가하고, 삭제 후 데이터를 다시 불러오는 로직을 구현했습니다.

배운 내용 요약 📝

삭제 기능 구현을 통해 사용자가 불필요한 데이터를 손쉽게 제거할 수 있는 방법을 배웠습니다. deleteCanvas 함수를 활용하여 서버에서 데이터를 삭제하고, UI에서 이를 반영하는 로직을 추가함으로써 애플리케이션의 완성도를 높일 수 있었습니다. 또한, 사용자 경험을 고려한 삭제 확인 메시지와 오류 처리 방식을 구현하여 보다 안정적인 기능을 제공할 수 있게 되었습니다.


타이틀 수정 📌✏️

이번 포스트에서는 타이틀 수정 기능 구현과 상세 조회 기능에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.

실습 🛠️


HTTP 메소드 개요 🌐

  • POST - 새로운 자원 생성
  • PUT - 기존 자원 전체 업데이트 또는 새 자원 생성
  • PATCH - 일부 수정

이러한 HTTP 메소드를 이해하고 적절히 활용하는 것은 RESTful API를 효과적으로 사용하는 데 중요합니다.

상세 조회 구현 🔍

src/api/canvas.js 수정 📄

상세 조회 기능을 구현하기 위해 getCanvasByIdupdateTitle 함수를 추가했습니다.

// ...생략...
export async function deleteCanvas(id) {
  await canvases.delete(`/${id}`);
}

export async function getCanvasById(id) {
  const { data } = await canvases.get(`/${id}`);
  return data;
}

export async function updateTitle(id, title) {
  /**
   * post - 새로운 자원 생성
   * put - 기존 자원 전체 업데이트 또는 새 자원 생성
   * patch - 일부 수정
   */
  await canvases.patch(`/${id}`, { title });
}

코드 설명:

  • getCanvasById: 특정 id를 가진 캔버스의 상세 정보를 가져옵니다.
  • updateTitle: 특정 id를 가진 캔버스의 title을 일부 수정합니다. 여기서는 PATCH 메소드를 사용하여 부분 업데이트를 수행합니다.
  • deleteCanvas: 특정 id를 가진 캔버스를 삭제합니다.

추가 설명:
PATCH 메소드를 사용하여 자원의 일부만 수정함으로써 불필요한 데이터 전송을 줄이고, 효율적인 업데이트가 가능해졌습니다.

src/components/CanvasTitle.jsx 구현 🎨

캔버스의 타이틀을 수정할 수 있는 컴포넌트를 구현했습니다.

import { useEffect, useState } from 'react';
import { FaCheck, FaEdit } from 'react-icons/fa';

function CanvasTitle({ value, onChange }) {
  const [title, setTitle] = useState(value);
  useEffect(() => {
    setTitle(value);
  }, [value]);

  const [isEditing, setIsEditing] = useState(false);

  const handleDoneTitle = () => {
    setIsEditing(false);
    onChange(title);
  };

  return (
    <div className="flex items-center justify-center mb-10">
      {isEditing ? (
        <div className="flex items-center">
          <input
            type="text"
            className="text-4xl font-bold text-center text-blue-600 bg-transparent border-b-2 border-blue-600 focus:outline-none"
            value={title}
            onChange={e => setTitle(e.target.value)}
          />
          <button
            className="ml-2 p-2 bg-green-500 text-white rounded-full hover:bg-green-600 transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
            aria-label="Save title"
            onClick={handleDoneTitle}
          >
            <FaCheck />
          </button>
        </div>
      ) : (
        <>
          <h1 className="text-4xl font-bold text-center ">{title}</h1>
          <button
            className="ml-2 p-2 bg-yellow-500 text-white rounded-full hover:bg-yellow-600 transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-opacity-50"
            aria-label="Edit title"
            onClick={() => setIsEditing(true)}
          >
            <FaEdit />
          </button>
        </>
      )}
    </div>
  );
}

export default CanvasTitle;

코드 설명:

  • CanvasTitle 컴포넌트는 타이틀을 표시하고 수정할 수 있는 기능을 제공합니다.
  • isEditing 상태를 통해 편집 모드와 보기 모드를 전환합니다.
  • 편집 모드에서는 입력 필드와 확인 버튼이 나타나며, 확인 버튼을 클릭하면 onChange 콜백이 호출되어 타이틀이 업데이트됩니다.
  • 보기 모드에서는 타이틀과 편집 버튼이 표시됩니다.

추가 설명:
이 컴포넌트를 통해 사용자는 캔버스의 타이틀을 직관적으로 수정할 수 있습니다. 아이콘 버튼을 활용하여 사용자 경험을 향상시켰습니다.

src/pages/CanvasDetail.jsx 수정 📄

상세 조회 페이지에서 타이틀 수정 기능을 통합했습니다.

import { useParams } from 'react-router-dom';
import CanvasTitle from '../components/CanvasTitle';
import LeanCanvas from '../components/LeanCanvas';
import { useEffect, useState } from 'react';
import { getCanvasById, updateTitle } from '../api/canvas';

function CanvasDetail() {
  const { id } = useParams();
  const [canvas, setCanvas] = useState();

  useEffect(() => {
    const fetchCanvas = async () => {
      const data = await getCanvasById(id);
      setCanvas(data);
    };
    fetchCanvas();
  }, [id]);

  const handleTitleChange = async title => {
    try {
      await updateTitle(id, title);
      setCanvas(prev => ({ ...prev, title }));
    } catch (err) {
      alert(err.message);
    }
  };

  return (
    <div>
      <CanvasTitle value={canvas?.title} onChange={handleTitleChange} />
      <LeanCanvas />
    </div>
  );
}

export default CanvasDetail;

코드 설명:

  • CanvasDetail 컴포넌트는 특정 id를 가진 캔버스의 상세 정보를 표시합니다.
  • useEffect를 사용하여 컴포넌트가 마운트될 때 getCanvasById 함수를 호출하여 데이터를 가져옵니다.
  • handleTitleChange 함수는 CanvasTitle 컴포넌트에서 전달된 새로운 타이틀을 받아 updateTitle 함수를 호출하여 서버에 업데이트를 요청합니다. 업데이트가 성공하면 로컬 상태도 함께 갱신합니다.

추가 설명:
상세 조회 페이지에서 타이틀 수정 기능을 통합함으로써 사용자는 캔버스의 타이틀을 직접 수정할 수 있습니다. 데이터의 일관성을 유지하기 위해 서버와 로컬 상태를 모두 업데이트하는 로직을 추가했습니다.

전체 수정 사항 요약 📋

  • src/api/canvas.jsgetCanvasByIdupdateTitle 함수를 추가하여 상세 조회 및 타이틀 수정 기능을 구현했습니다.
  • src/components/CanvasTitle.jsx 컴포넌트를 추가하여 타이틀을 수정할 수 있는 UI를 제공했습니다.
  • src/pages/CanvasDetail.jsx를 수정하여 상세 조회 페이지에 타이틀 수정 기능을 통합했습니다.
  • HTTP 메소드의 역할을 명확히 이해하고 적절히 활용하여 RESTful API를 효과적으로 구현했습니다.

배운 내용 요약 📝

타이틀 수정 기능과 상세 조회 기능을 구현하는 과정을 통해 RESTful API의 다양한 HTTP 메소드를 이해하고 적용하는 방법을 배웠습니다. 특히, PATCH 메소드를 활용하여 자원의 일부를 효율적으로 수정하는 방법을 익혔으며, React 컴포넌트를 통해 사용자 인터페이스에서 직관적으로 타이틀을 수정할 수 있는 기능을 구현했습니다. 상세 조회 페이지에서 데이터의 일관성을 유지하기 위한 상태 관리 방법을 학습함으로써 더욱 완성도 높은 애플리케이션을 개발할 수 있게 되었습니다.

동기 vs 비동기 & async/await 설명 🔄

프로그래밍에서 동기(Synchronous)비동기(Asynchronous)는 작업이 실행되는 방식에 차이를 보입니다.

  • 동기: 작업이 순차적으로 진행되며, 이전 작업이 완료될 때까지 다음 작업이 시작되지 않습니다. 예를 들어, 파일을 읽는 작업을 동기로 처리하면, 파일 읽기가 완료될 때까지 다른 작업을 수행할 수 없습니다.
  • 비동기: 작업이 병렬적으로 진행되며, 이전 작업이 완료되지 않아도 다음 작업을 시작할 수 있습니다. 비동기 작업은 주로 네트워크 요청, 파일 I/O 등 시간이 걸리는 작업에 사용됩니다.

async/await는 비동기 코드를 보다 간결하고 읽기 쉽게 작성할 수 있도록 도와주는 문법입니다.

  • async 함수: 함수 앞에 async 키워드를 붙이면, 해당 함수는 항상 Promise를 반환합니다.
  • await 키워드: async 함수 내에서만 사용할 수 있으며, Promise가 처리될 때까지 함수의 실행을 일시 중지합니다. 이를 통해 비동기 작업을 동기 코드처럼 작성할 수 있습니다.

예시:

// 비동기 함수 예시
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();

설명:
위 예시에서 fetchData 함수는 async로 선언되어 있으며, await를 사용하여 fetch 요청이 완료될 때까지 기다립니다. 이로 인해 비동기 작업이 완료된 후에야 다음 줄의 코드가 실행됩니다. 이를 통해 비동기 코드를 보다 직관적이고 관리하기 쉽게 작성할 수 있습니다.

동기와 비동기의 개념을 명확히 이해하고, async/await를 적절히 활용함으로써 효율적이고 가독성 높은 코드를 작성할 수 있습니다.


린캔버스 상세 📊🔍

이번 포스트에서는 린캔버스의 상세 화면 UI 구현에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.

실습 🛠️


상세화면 UI 구현 📋

린캔버스의 상세 화면을 구현하기 위해 여러 파일을 수정하고 새로운 컴포넌트를 추가했습니다. 주요 변경사항과 그에 대한 설명은 다음과 같습니다.

data/db.json 수정 📄

린캔버스 데이터를 저장하는 db.json 파일에 상세 데이터를 추가했습니다.

//...생략...
{
      "id": 5,
      "title": "혁신적인 스마트홈 솔루션!!",
      "lastModified": "2023-08-22",
      "category": "신규",
      "problem": {
        "notes": [
          {
            "id": "p1",
            "content": "기존 스마트홈 시스템의 복잡성",
            "color": "bg-yellow-300"
          },
          {
            "id": "p2",
            "content": "높은 초기 설치 비용",
            "color": "bg-pink-300"
          },
          {
            "id": "p3",
            "content": "다양한 기기 간 호환성 문제",
            "color": "bg-blue-300"
          }
        ]
      },
      "customerSegments": {
        "notes": [
          {
            "id": "cs1",
            "content": "30-50대 중산층 가정",
            "color": "bg-blue-300"
          },
          {
            "id": "cs2",
            "content": "기술에 관심 있는 밀레니얼 세대",
            "color": "bg-yellow-300"
          },
          {
            "id": "cs3",
            "content": "에너지 절약에 관심 있는 환경 의식적 소비자",
            "color": "bg-pink-300"
          }
        ]
      },
      "valueProposition": {
        "notes": [
          {
            "id": "vp1",
            "content": "직관적이고 사용하기 쉬운 인터페이스",
            "color": "bg-pink-300"
          },
          {
            "id": "vp2",
            "content": "저렴한 초기 비용과 월 구독 모델",
            "color": "bg-yellow-300"
          },
          {
            "id": "vp3",
            "content": "모든 주요 스마트홈 기기와 호환",
            "color": "bg-blue-300"
          }
        ]
      },
      "solution": {
        "notes": [
          {
            "id": "s1",
            "content": "AI 기반 자동화 시스템",
            "color": "bg-yellow-300"
          },
          {
            "id": "s2",
            "content": "클라우드 기반 중앙 제어 시스템",
            "color": "bg-blue-300"
          },
          {
            "id": "s3",
            "content": "모듈식 설계로 쉬운 확장성",
            "color": "bg-pink-300"
          }
        ]
      },
      "unfairAdvantage": {
        "notes": [
          {
            "id": "ua1",
            "content": "특허받은 AI 알고리즘",
            "color": "bg-pink-300"
          },
          {
            "id": "ua2",
            "content": "주요 스마트홈 기기 제조업체와의 독점 파트너십",
            "color": "bg-yellow-300"
          }
        ]
      },
      "channels": {
        "notes": [
          {
            "id": "ch1",
            "content": "온라인 직접 판매",
            "color": "bg-yellow-300"
          },
          {
            "id": "ch2",
            "content": "전문 설치 업체 네트워크",
            "color": "bg-blue-300"
          },
          {
            "id": "ch3",
            "content": "대형 전자제품 소매점",
            "color": "bg-pink-300"
          }
        ]
      },
      "keyMetrics": {
        "notes": [
          {
            "id": "km1",
            "content": "월간 활성 사용자 수",
            "color": "bg-blue-300"
          },
          {
            "id": "km2",
            "content": "고객 유지율",
            "color": "bg-yellow-300"
          },
          {
            "id": "km3",
            "content": "평균 에너지 절감률",
            "color": "bg-pink-300"
          }
        ]
      },
      "costStructure": {
        "notes": [
          {
            "id": "cs1",
            "content": "하드웨어 개발 및 생산 비용",
            "color": "bg-pink-300"
          },
          {
            "id": "cs2",
            "content": "소프트웨어 개발 및 유지보수",
            "color": "bg-blue-300"
          },
          {
            "id": "cs3",
            "content": "마케팅 및 고객 지원",
            "color": "bg-yellow-300"
          }
        ]
      },
      "revenueStreams": {
        "notes": [
          {
            "id": "rs1",
            "content": "하드웨어 판매",
            "color": "bg-yellow-300"
          },
          {
            "id": "rs2",
            "content": "월간 구독료",
            "color": "bg-pink-300"
          },
          {
            "id": "rs3",
            "content": "프리미엄 기능 업그레이드",
            "color": "bg-blue-300"
          }
        ]
      },
      "existingAlternatives": {
        "notes": [
          {
            "id": "ea1",
            "content": "기존 홈 오토메이션 시스템",
            "color": "bg-yellow-300"
          },
          {
            "id": "ea2",
            "content": "개별 스마트 기기 사용",
            "color": "bg-pink-300"
          }
        ]
      },
      "highLevelConcept": {
        "notes": [
          {
            "id": "hlc1",
            "content": "모든 가정을 위한 AI 기반 스마트홈",
            "color": "bg-blue-300"
          }
        ]
      },
      "earlyAdopters": {
        "notes": [
          {
            "id": "ea1",
            "content": "테크 얼리어답터",
            "color": "bg-blue-300"
          },
          {
            "id": "ea2",
            "content": "에너지 절약 열성 지지자",
            "color": "bg-pink-300"
          }
        ]
      }
    }

src/pages/CanvasDetail.jsx 수정 📄

상세 화면 페이지에서 린캔버스의 상세 정보를 표시하도록 수정했습니다.

import { useParams } from 'react-router-dom';
import CanvasTitle from '../components/CanvasTitle';
import LeanCanvas from '../components/LeanCanvas';
import { useEffect, useState } from 'react';
import { getCanvasById, updateTitle } from '../api/canvas';

function CanvasDetail() {
  const { id } = useParams();
  const [canvas, setCanvas] = useState();

  useEffect(() => {
    const fetchCanvas = async () => {
      const data = await getCanvasById(id);
      setCanvas(data);
    };
    fetchCanvas();
  }, [id]);

  const handleTitleChange = async title => {
    try {
      await updateTitle(id, title);
      setCanvas(prev => ({ ...prev, title }));
    } catch (err) {
      alert(err.message);
    }
  };
  return (
    <div>
      <CanvasTitle value={canvas?.title} onChange={handleTitleChange} />
      **{canvas && <LeanCanvas canvas={canvas} />}**
    </div>
  );
}

export default CanvasDetail;

코드 설명:

  • CanvasDetail 컴포넌트는 특정 id를 가진 린캔버스의 상세 정보를 가져와 표시합니다.
  • useEffect 훅을 사용하여 컴포넌트가 마운트될 때 getCanvasById 함수를 호출하여 데이터를 가져옵니다.
  • handleTitleChange 함수는 CanvasTitle 컴포넌트에서 전달된 새로운 타이틀을 받아 updateTitle 함수를 호출하여 서버에 업데이트를 요청합니다. 업데이트가 성공하면 로컬 상태도 함께 갱신합니다.
  • {canvas && <LeanCanvas canvas={canvas} />} 부분은 canvas 데이터가 로드된 후에만 LeanCanvas 컴포넌트를 렌더링합니다.

추가 설명:
상세 조회 페이지에서 타이틀 수정 기능을 통합함으로써 사용자는 린캔버스의 타이틀을 직접 수정할 수 있습니다. 데이터의 일관성을 유지하기 위해 서버와 로컬 상태를 모두 업데이트하는 로직을 추가했습니다.

src/pages/components/LeanCanvas.jsx 수정 📄

린캔버스의 각 섹션을 카드 형식으로 표시하는 LeanCanvas 컴포넌트를 구현했습니다.

import CanvasCard from './CanvasCard';
function LeanCanvas({ canvas }) {
  return (
    <div className="border-4 border-black">
      <div className="grid grid-cols-5">
        **<CanvasCard title={'1. 문제'} notes={canvas.problem.notes} />
        <CanvasCard title={'4. 해결안'} notes={canvas.solution.notes} />
        <CanvasCard
          title={'3. 가치제안'}
          notes={canvas.valueProposition.notes}
        />
        <CanvasCard
          title={'5. 경쟁우위'}
          notes={canvas.unfairAdvantage.notes}
        />
        <CanvasCard
          title={'2. 목표 고객'}
          notes={canvas.customerSegments.notes}
        />
        <CanvasCard
          title={'기존 대안'}
          isSubtitle
          notes={canvas.existingAlternatives.notes}
        />
        <CanvasCard title={'8. 핵심지표'} notes={canvas.keyMetrics.notes} />
        <CanvasCard
          title={'상위개념'}
          isSubtitle
          notes={canvas.highLevelConcept.notes}
        />
        <CanvasCard title={'9. 고객 경로'} notes={canvas.channels.notes} />
        <CanvasCard
          title={'얼리 어답터'}
          isSubtitle
          notes={canvas.earlyAdopters.notes}
        />**
      </div>
      <div className="grid grid-cols-2">
        **<CanvasCard title={'7. 비용 구조'} notes={canvas.costStructure.notes} />
        <CanvasCard
          title={'6. 수익 흐름'}
          notes={canvas.revenueStreams.notes}
        />**
      </div>
    </div>
  );
}

export default LeanCanvas;

코드 설명:

  • LeanCanvas 컴포넌트는 린캔버스의 각 섹션을 CanvasCard 컴포넌트를 사용하여 그리드 레이아웃으로 표시합니다.
  • 각 섹션의 제목과 노트를 CanvasCard에 전달하여 일관된 스타일로 렌더링합니다.
  • isSubtitle prop을 사용하여 특정 섹션의 스타일을 다르게 적용할 수 있습니다.

추가 설명:
린캔버스의 각 섹션을 시각적으로 구분하여 사용자에게 명확한 정보를 제공합니다. 그리드 레이아웃을 사용하여 깔끔하고 정돈된 UI를 구현했습니다.

src/components/CanvasCard.jsx 수정 📄

린캔버스의 각 섹션을 카드 형식으로 표시하는 CanvasCard 컴포넌트를 구현했습니다.

import { FaPlus } from 'react-icons/fa';
import Note from './Note';
function CanvasCard({ title, isSubtitle = false, notes = [] }) {
  **const handleAddNote = () => {};
  const handleRemoveNote = id => {};**
  return (
    <div className="row-span-1 bg-white min-h-48 border border-collapse border-gray-300">
      <div
        className={`${isSubtitle === false && 'bg-gray-100 border-b border-b-gray-300'} flex items-start justify-between px-3 py-2`}
      >
        <h3 className={`${isSubtitle === false && 'font-bold'} `}>{title}</h3>
        <button
          className="bg-blue-400  text-white p-1.5 text-xs rounded-md"
          onClick={handleAddNote}
        >
          <FaPlus />
        </button>
      </div>
      <div className="space-y-3 min-h-32 p-3">
        {notes.map(note => (
          <Note
            key={note.id}
            id={note.id}
            content={note.content}
            **color={note.color}**
            onRemoveNote={handleRemoveNote}
          />
        ))}
      </div>
    </div>
  );
}

export default CanvasCard;

코드 설명:

  • CanvasCard 컴포넌트는 린캔버스의 각 섹션을 카드 형식으로 표시합니다.
  • titlenotes를 props로 받아, 제목과 노트를 렌더링합니다.
  • handleAddNotehandleRemoveNote 함수는 노트 추가 및 삭제 기능을 처리합니다.

추가 설명:
이 컴포넌트를 통해 린캔버스의 각 섹션을 일관된 스타일로 표시할 수 있습니다. 추가 버튼을 통해 노트를 동적으로 추가할 수 있는 기능을 구현할 수 있습니다.

src/components/Note.jsx 구현 🎨

린캔버스의 노트를 표시하고 수정할 수 있는 Note 컴포넌트를 구현했습니다.

import { useEffect, useRef, useState } from 'react';
import { AiOutlineClose, AiOutlineCheck } from 'react-icons/ai';

const Note = ({ id, content, color: initalColor, onRemoveNote }) => {
  const colorOptions = [
    'bg-yellow-300',
    'bg-pink-300',
    'bg-blue-300',
    'bg-green-300',
  ];

  const [color, setColor] = useState(() => {
    if (initalColor) return initalColor;
    const randomIndex = Math.floor(Math.random() * colorOptions.length);
    return colorOptions[randomIndex];
  });

  const [isEditing, setIsEditing] = useState(false);

  const textareaRef = useRef(null);

  useEffect(() => {
    if (textareaRef.current) {
      textareaRef.current.style.height =
        textareaRef.current.scrollHeight + 'px';
    }
  }, [content]);

  return (
    <div
      className={`p-4 ${color} relative max-h-[32rem] overflow-hidden`}
      onClick={() => setIsEditing(true)}
    >
      <div className="absolute top-2 right-2">
        {isEditing ? (
          <button
            aria-label="Check Note"
            className="text-gray-700"
            onClick={e => {
              e.stopPropagation();
              setIsEditing(false);
            }}
          >
            <AiOutlineCheck size={20} />
          </button>
        ) : (
          <button
            aria-label="Close Note"
            className="text-gray-700"
            onClick={() => onRemoveNote(id)}
          >
            <AiOutlineClose size={20} />
          </button>
        )}
      </div>
      <textarea
        ref={textareaRef}
        value={content}
        className={`w-full h-full bg-transparent resize-none border-none focus:outline-none text-gray-900 overflow-hidden`}
        aria-label="Edit Note"
        placeholder="메모를 작성하세요."
        style={{ height: 'auto', minHeight: '8rem' }}
        readOnly={!isEditing}
      />
      {isEditing && (
        <div className="flex space-x-2">
          {colorOptions.map((option, index) => (
            <button
              key={index}
              className={`w-6 h-6 rounded-full cursor-pointer outline outline-gray-50 ${option}`}
              onClick={() => setColor(option)}
              aria-label={`Change color to ${option}`}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default Note;

코드 설명:

  • Note 컴포넌트는 개별 노트를 표시하며, 사용자가 클릭하면 편집 모드로 전환됩니다.
  • colorOptions 배열을 통해 노트의 배경색을 선택할 수 있습니다.
  • isEditing 상태를 통해 편집 모드와 보기 모드를 전환합니다.
  • 편집 모드에서는 텍스트를 수정할 수 있으며, 색상 변경 버튼을 제공합니다.
  • 노트 삭제 버튼을 통해 노트를 제거할 수 있습니다.

추가 설명:
이 컴포넌트를 통해 사용자는 린캔버스의 각 노트를 직관적으로 수정하고 관리할 수 있습니다. 색상 선택 기능을 통해 노트의 시각적 구분을 용이하게 했습니다.

전체 수정 사항 요약 📋

  • data/db.json에 린캔버스 상세 데이터를 추가하여 실제 데이터를 기반으로 UI를 구현했습니다.
  • src/pages/CanvasDetail.jsx를 수정하여 린캔버스의 상세 정보를 표시하고, 타이틀 수정 기능을 통합했습니다.
  • src/pages/components/LeanCanvas.jsx, src/components/CanvasCard.jsx, src/components/Note.jsx를 추가 및 수정하여 린캔버스의 각 섹션과 노트를 시각적으로 구분하여 표시했습니다.
  • React 컴포넌트를 활용하여 사용자 인터페이스에서 린캔버스의 데이터를 직관적으로 관리하고 수정할 수 있는 기능을 구현했습니다.
profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글