App.jsx
import { useState } from "react";
import { v4 as uuid } from "uuid";
import Sidebar from "./components/Sidebar";
import NoSelected from "./components/NoSelected";
import AddProject from "./components/AddProject";
import ProjectDetail from "./components/ProjectDetail";
function App() {
const [selected, setSelected] = useState({ selected: "none", project: {} });
const [projects, setProjects] = useState([]);
const handleCancel = () => {
setSelected((preData) => {
return { ...preData, ["selected"]: "none" };
})
}
const handleAdd = () => {
setSelected((preData) => {
return { ...preData, ["selected"]: "add" };
})
}
const handleSave = (title, description, date) => {
setProjects((preData) => {
return [...preData, { id: uuid(), title: title, description: description, date: date, tasks: new Array() }];
})
setSelected((preData) => {
return { ...preData, ["selected"]: "none" };
});
}
const handleProjectClick = (project) => {
setSelected((preData) => {
return { selected: "project", project: project };
})
}
const handleDeleteProject = (id) => {
console.log('deleted');
setProjects((preData) => {
return preData.filter((project) => project.id !== id);
});
handleCancel();
}
const handleTaskClear = (id, idx) => {
setProjects((preData) => {
return preData.map((project) => {
project['id'] === id ? project['tasks'] = project['tasks'].filter((task, i) => i !== idx) : '';
return project;
})
})
}
const handleAddTask = (id, task) => {
setProjects((preData) => {
return preData.map((project) => {
project['id'] === id ? project['tasks'].push(task) : '';
return project;
})
})
}
return (
<>
<div className="flex flex-row h-screen">
<Sidebar handleAdd={handleAdd} projects={projects} handleProjectClick={handleProjectClick} />
{selected["selected"] === "none" && <NoSelected handleAdd={handleAdd} />}
{selected["selected"] === "add" && <AddProject handleCancel={handleCancel} handleSave={handleSave} />}
{selected["selected"] === "project" &&
<ProjectDetail
project={selected["project"]}
handleDeleteProject={handleDeleteProject}
handleTaskClear={handleTaskClear}
handleAddTask={handleAddTask} />}
</div>
</>
);
}
export default App;
Sidebar.jsx
export default function Sidebar({ handleAdd, projects, handleProjectClick }) {
return (
<aside className="mt-10 rounded-tr-3xl bg-black w-1/5 min-w-64 text-center ">
<div className="mx-7 text-start">
<h1 className="text-2xl font-bold my-10 text-white">YOUR PROJECTS</h1>
<button onClick={handleAdd} className="rounded px-5 py-2 bg-gray-800 text-gray-400 text-lg">+ Add Project</button>
<ul className="py-16">
{projects.map((value) => {
return <li className="text-gray-400 hover:bg-gray-800 mt-2">
<button onClick={() => handleProjectClick(value)} className="px-3 py-1">{value['title']}</button>
</li>
})}
</ul>
</div>
</aside>
)
}
NoSelected.jsx
import image from "../assets/no-projects.png";
export default function NoSelected({ handleAdd }) {
return (
<main className="text-center m-auto">
<img src={image} alt="" className="w-20 m-auto" />
<h2 className="my-4 text-gray-600 font-extrabold text-2xl">No Project Selected</h2>
<p className="text-gray-500 font-medium text-lg">Select a project or get started with a new one</p>
<button onClick={handleAdd} className="mt-10 text-lg bg-black rounded-lg font-medium text-gray-400 px-5 py-3">Create new project</button>
</main>
)
}
AddProject.jsx
import { useRef } from "react";
export default function AddProject({ handleCancel, handleSave }) {
const titleRef = useRef(null);
const descriptionRef = useRef(null);
const dateRef = useRef(null);
const inputBoxClass = "my-5";
const labelClass = "block mb-1 text-base text-gray-500 font-bold";
const inputClass = "px-3 w-full bg-gray-200 border-b-2 border-gray-300 focus:border-gray-600 p-1";
return (
<main className="ml-12 mt-24 w-2/4">
<div className="text-end">
<button onClick={handleCancel} className="px-7 py-2 text-lg rounded text-black">Cancel</button>
<button
onClick={() => handleSave(titleRef.current.value, descriptionRef.current.value, dateRef.current.value)}
className="px-7 py-2 text-lg rounded bg-black text-white">
Save
</button>
</div>
<div className={inputBoxClass}>
<label htmlFor="title" className={labelClass}>TITLE</label>
<input ref={titleRef} type="text" id="title" className={inputClass + " h-9"} />
</div>
<div className={inputBoxClass}>
<label htmlFor="description" className={labelClass}>DESCRIPTION</label>
<textarea ref={descriptionRef} name="" id="description" rows="5" className={inputClass}></textarea>
</div>
<div className={inputBoxClass}>
<label htmlFor="date" className={labelClass}>DUE DATE</label>
<input ref={dateRef} type="date" id="date" className={inputClass} />
</div>
</main>
)
}
ProjectDetail.jsx
import { useRef } from "react";
export default function ProjectDetail({ project, handleDeleteProject, handleAddTask, handleTaskClear }) {
const taskInputRef = useRef();
const brDescription = project['description'].split('\n').map((line) => {
return <span>{line}<br /></span>
});
const handleTaskSubmit = (e) => {
e.preventDefault(); // onSubmit이라서 기본 이벤트를 막지 않으면 에러가 발생
const newTask = taskInputRef.current.value;
taskInputRef.current.value = '';
handleAddTask(project['id'], newTask);
}
return (
<main className="mt-24 ml-12 w-2/5">
<div className="flex flex-row justify-between">
<h2 className="text-4xl font-extrabold">{project['title']}</h2>
<button onClick={() => handleDeleteProject(project['id'])} className="font-semibold text-gray-600">Delete</button>
</div>
<p className="my-3 text-lg text-gray-500">{project['date']}</p>
{/* 리액트는 개행 문자를 무시하고 출력하기 때문에 내가 직접 개행해줘야한다. */}
{/* <p className="my-8 text-lg font-semibold text-gray-700">{project['description']}</p> */}
<p className="my-8 text-lg font-semibold text-gray-700">{brDescription}</p>
<hr className="h-0.5 bg-gray-400" />
<h2 className="my-6 text-3xl font-extrabold">Tasks</h2>
<form onSubmit={(e) => handleTaskSubmit(e)}>
<input type="text" ref={taskInputRef} className="px-3 bg-gray-200 h-9 w-1/2" />
<button className="ml-4 font-semibold">Add Task</button>
</form>
{project['tasks'].length ?
<ul className="mt-8 px-4 py-4 bg-gray-100">
{project['tasks'].map((task, idx) =>
<li className="flex flex-row justify-between my-3"><p className="font-semibold">{task}</p>
<button onClick={() => handleTaskClear(project['id'], idx)} className="ml-3 font-semibold">Clear</button>
</li>)}
</ul> :
<p className="mt-4 text-lg font-medium">This project does not have any tasks yet.</p>}
</main>
)
}
주절
과제하기 싫어서 미루고 미루고 미루고 미루다가 오늘 끝냈다. 코드가 마음에 안들고 변수명도 마음에 안들지만 언젠가 고치도록 하겠다. 아래는 고난 일지다.
- 구조 분해 하는 거 까먹어서 한참 고생했다. 다른 컴포넌트로 project를 넘겨주는데 projeect 안에 또 project가 들어있길래 왜 이것만 이러는 거지 싶었는데 내가 구조 분해 안 해준 것이었다.
- 원래 task 추가하는 거를 아무 생각 없이 그냥 버튼에다 onClick 이벤트를 줬었다. 그러면 엔터 치면 자동으로 추가가 안 돼서 form으로 고쳤는데 task 추가하자마자 프로젝트가 통째로 삭제되는 것이다. 이게 무슨 미친 경우인가 하면서 프로젝트 삭제하는 코드가 실행된 건가 싶어서 콘솔에 찍어봤는데 실행되지 않았었다. 그럼 왜 이 지경인가 해서 콘솔에 이거 저거 찍다가 Uncatched가 찰나의 순간 찍히고 사라지는 걸 봤다. form의 문제임을 확신하고 검색해 보니 내가 preventDefault를 해주지 않은 것이었다. 그렇다 모든 것은 나의 잘못
- 프로젝트 description에 분명 개행을 했는데 저장하고 다시 출력할 때는 개행 문자가 출력되지 않았다. 이건 또 왜이런가 검색해 봤더니 리액트는 보안 문제 때문에 텍스트만 출력한다고 한다. 그렇다고 textarea에 개행 문자가 저장 안 되는 건 아니기 때문에 개행 문자 단위로 split 하고 출력할 때 내가 개행 문자 넣어주면 된다.
'웹 프로그래밍' 카테고리의 다른 글
[Spring Boot] Licruit - 회원가입 (0) | 2024.09.10 |
---|---|
[Spring Boot] API 생성 (0) | 2024.07.08 |
[React] Investment Calculator (0) | 2024.01.08 |
[YelpCamp 프로젝트] 보안 (0) | 2023.12.11 |
[Yelpcamp 프로젝트] 디자인 수정 (0) | 2023.12.10 |