이번 포스트에서는 삭제 기능 구현에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.
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
를 사용하여 해당 캔버스를 삭제합니다.추가 설명:
삭제 기능을 구현함으로써 사용자가 더 이상 필요하지 않은 캔버스를 손쉽게 제거할 수 있게 되었습니다. 이는 데이터 관리의 효율성을 높이고, 사용자 경험을 향상시키는 중요한 기능입니다.
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.js
에 deleteCanvas
함수를 추가하여 서버에서 캔버스를 삭제할 수 있는 기능을 구현했습니다.src/pages/Home.jsx
에 handleDeleteItem
함수를 추가하여 사용자 인터페이스에서 캔버스를 삭제할 수 있도록 했습니다.삭제 기능 구현을 통해 사용자가 불필요한 데이터를 손쉽게 제거할 수 있는 방법을 배웠습니다. deleteCanvas
함수를 활용하여 서버에서 데이터를 삭제하고, UI에서 이를 반영하는 로직을 추가함으로써 애플리케이션의 완성도를 높일 수 있었습니다. 또한, 사용자 경험을 고려한 삭제 확인 메시지와 오류 처리 방식을 구현하여 보다 안정적인 기능을 제공할 수 있게 되었습니다.
이번 포스트에서는 타이틀 수정 기능 구현과 상세 조회 기능에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.
이러한 HTTP 메소드를 이해하고 적절히 활용하는 것은 RESTful API를 효과적으로 사용하는 데 중요합니다.
src/api/canvas.js
수정 📄상세 조회 기능을 구현하기 위해 getCanvasById
와 updateTitle
함수를 추가했습니다.
// ...생략...
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.js
에 getCanvasById
와 updateTitle
함수를 추가하여 상세 조회 및 타이틀 수정 기능을 구현했습니다.src/components/CanvasTitle.jsx
컴포넌트를 추가하여 타이틀을 수정할 수 있는 UI를 제공했습니다.src/pages/CanvasDetail.jsx
를 수정하여 상세 조회 페이지에 타이틀 수정 기능을 통합했습니다.타이틀 수정 기능과 상세 조회 기능을 구현하는 과정을 통해 RESTful API의 다양한 HTTP 메소드를 이해하고 적용하는 방법을 배웠습니다. 특히, PATCH
메소드를 활용하여 자원의 일부를 효율적으로 수정하는 방법을 익혔으며, React 컴포넌트를 통해 사용자 인터페이스에서 직관적으로 타이틀을 수정할 수 있는 기능을 구현했습니다. 상세 조회 페이지에서 데이터의 일관성을 유지하기 위한 상태 관리 방법을 학습함으로써 더욱 완성도 높은 애플리케이션을 개발할 수 있게 되었습니다.
async/await
설명 🔄프로그래밍에서 동기(Synchronous)와 비동기(Asynchronous)는 작업이 실행되는 방식에 차이를 보입니다.
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 구현에 대해 다루었습니다. 교안을 바탕으로 코드의 주요 변경사항과 그에 대한 설명을 포함하여 내용을 정리해보겠습니다.
린캔버스의 상세 화면을 구현하기 위해 여러 파일을 수정하고 새로운 컴포넌트를 추가했습니다. 주요 변경사항과 그에 대한 설명은 다음과 같습니다.
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
컴포넌트는 린캔버스의 각 섹션을 카드 형식으로 표시합니다.title
과 notes
를 props로 받아, 제목과 노트를 렌더링합니다.handleAddNote
와 handleRemoveNote
함수는 노트 추가 및 삭제 기능을 처리합니다.추가 설명:
이 컴포넌트를 통해 린캔버스의 각 섹션을 일관된 스타일로 표시할 수 있습니다. 추가 버튼을 통해 노트를 동적으로 추가할 수 있는 기능을 구현할 수 있습니다.
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
를 추가 및 수정하여 린캔버스의 각 섹션과 노트를 시각적으로 구분하여 표시했습니다.