Header 만들기
헤더에는 3개의 섹션이 있다.
1. 가운데 : 초기화면은 오늘의 날짜가 나온다.
2. 왼쪽 : 월이 -1 된다.
3. 오른쪽 : 월이 +1 된다.
Home.js
const Home = () => {
// context로 데이터 받아오기
const diaryList = useContext(DiaryStateContext);
// 헤더 날짜
// 날짜를 저장할 State
const [curDate, setCurDate] = useState(new Date());
// 헤더에 나타날 날짜 (javaScript에서는 月월 표현할 때 0부터 시작하기 때문에 +1을 해주어야 이번달이 나옴)
const headText = `${curDate.getFullYear()}년 ${curDate.getMonth()+1}월`
// 왼쪽, 오른쪽 버튼 눌러 月 감소, 증가
const increaseMonth = () => {
setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()+1, curDate.getDate()));
}
const decreaseMonth = () => {
setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()-1, curDate.getDate()));
}
return (
<div className="Home">
<MyHeader headText={headText}
leftChild={<MyButton text={"<"} onClick={decreaseMonth}/>}
rightChild={<MyButton text={">"} onClick={increaseMonth}/>}
/>
</div>
)
}
DiaryList 만들기
Home화면에는 상단의 Header와 다이어리가 조회되는 DiaryList가 존재한다. DiaryList는 Header와 다른 여러 기능이 있기 때문에 Home에 만들지 않고 따로 컴포넌트를 만들어 관리해준다.
DiaryItem 만들기
이것 또한 하나의 컴포넌트이기 때문에 새로 컴포넌트를 생성하여 사용해야한다.
// Home.js
const Home = () => {
return (
<div className="Home">
<MyHeader headText={headText}
leftChild={<MyButton text={"<"} onClick={decreaseMonth}/>}
rightChild={<MyButton text={">"} onClick={increaseMonth}/>}
/>
<DiaryList diaryList={diaryList} />
</div>
)
}
// DiaryList.js
const DiaryList = ({diaryList}) => {
return <div className="DiaryList">
{ diaryList.map((it) =>
<DiaryItem key={it.id} {...it}/>
)}
</div>
}
Home에서 Context로 받아온 데이터를 DiaryList로 전달하고 map을 사용하여 각각의 item의 데이터를 DiaryItem으로 전달한다.
1. 날짜
data에 저장된 날짜는 밀리세컨즈 단위이기 때문에 변환이 필요하다.
const DiaryItem = ({id, date, subject, goal, content}) => {
// 날짜 변환
const dateStr= new Date(date)
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return
<div className="diary_date">
{`${dateStr.getMonth()+1}/${dateStr.getDate()} ${daysOfWeek[dateStr.getDay()]}`}
</div>
}
new Date를 사용하여 변환을 하면 ' Tue Jul 25 2023 00:00:00 GMT+0900 (한국 표준시) ' 이렇게 결과가 나오는데, 이것을 정리하여 띄워주면 된다. 이때, 요일은 getDay()로 가져오게되면 0~6의 숫자가 나오기 때문에 요일을 저장한 배열을 만들어 사용했다.
2. 주제
주제는 공부, 운동, 일상, 저축 4가지가 있는데 저장한 값에 따라 다른 이미지가 나오도록 하였다.
const DiaryItem = ({id, date, subject, goal, content}) => {
// 날짜 변환
const dateStr= new Date(date)
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return
<div className="diary_subject">
<img src={process.env.PUBLIC_URL + `assets/${subject}.png`} />
</div>
}
이미지 이름을 주제이름으로 해놓고, 경로에 백틱을 사용하여 지정하였다.
3. 달성률
달성한 목표의 개수 / 총 목표의 개수를 계산하여 그 %만큼 width 속성을 주어 게이지처럼 구현하였다.
const DiaryItem = ({id, date, subject, goal, content}) => {
// 달성률 계산 :
const achievement = Math.floor(((goal.map((it) => it.isComplete).filter((it)=>
it === true).length) / goal.length) * 100);
return
<span style={{ width : `${achievement}%`}}>
{achievement}%
</span>
}
4. 수정/삭제 드롭다운
드롭다운은 일기 상세페이지에서도 사용할 것이기 때문에 따로 컴포넌트로 만들어 사용하였다.
//DiaryItem.js
const DiaryItem = ({id, date, subject, goal, content}) => {
// 드롭다운 State : 드롭다운 버튼을 클릭하면 true, 다시 클릭하면 false로 하여 드롭다운이 보이고, 숨겨지게하였다.
const [drop, setDrop] = useState(false);
return
<div onClick={()=> setDrop(!drop)} className="dropdown_wrapper">
<span>{drop && <Dropdown id={id}/>}</span>
<span><FontAwesomeIcon icon={faEllipsis} /></span>
</div>
}
//Dropdown.js
const Dropdown = ({id}) => {
// 수정을 누르면 수정화면으로 넘어가기 위해 navigate를 사용하였다.
const navigate = useNavigate();
// App.js의 onDelete를 가져와서 데이터를 전달하였다.
const {onDelete} = useContext(DiaryDispatchContext);
const handleDelete = () => {
if(window.confirm("일기를 삭제하시겠습니까?")) {
onDelete(id);
}
}
return <div className="Dropdown">
<li onClick={()=>navigate(`/Edit/${id}`)}>수정</li>
<li onClick={handleDelete}>삭제</li>
</div>
}
4. 목표
체크박스를 클릭하면 isComplete의 값이 반전이 되도록 하여야 했는데, 이는 data의 state를 변경하는 것이기 때문에 App.js에서 함수를 만들고, useContext를 통해 DiaryItem으로 가져와 데이터를 전달하여야 했다.
DiaryItem.js
const DiaryItem = ({id, date, subject, goal, content}) => {
// 체크박스 클릭 시 완료
const {toggleComplete} = useContext(DiaryDispatchContext);
const handleComplete = (e) => {
const targetGoalId = e.target.getAttribute("data-goalid");
toggleComplete(id, targetGoalId);
}
return
<div className="diary_goal_wrapper">
{goal.map((it)=> (
<div className="diary_gaol">
<span key={it.goalId} onClick={handleComplete} >
{it.isComplete ? <FontAwesomeIcon icon={faSquareCheck} data-goalid={it.goalId}/>
: <FontAwesomeIcon icon={faSquare} data-goalid={it.goalId}/> }
</span>
<span className={`diary_gaol_${it.isComplete}`}>
{it.goalContent.length >= 15 ? it.goalContent.slice(0,15) + "..." : it.goalContent }
</span>
</div>
))}
</div>
}
App.js
데이터는 아래와 같은 형태를 하고 있는데
const dummyData = [
{
id : 1,
date : 1690210800000,
subject : "study",
goal : [
{
goalId : "study1",
goalContent : "영단어 20개 암기",
isComplete : true
},
{
goalId : "study2",
goalContent : "영어 일기 쓰기",
isComplete : true
},
{
goalId : "study3",
goalContent : "인강 5개 듣기",
isComplete : false
}
],
content : "인강 5개는 무리였다. 다음엔 목표를 작게 잡아야겠다."
},
]
처음에는 goal의 데이터와 goalId에만 집중을 하고있어서 계속 undifined이 뜨고 그래서 map을 사용할 수 없다는 오류가 한 100번은 떴다.
다시 데이터를 잘 생각해보니 위와 같은 구조를 하고 있기 때문에, 먼저 map을 통해 data에 접근하여 해당 요소의 data-goal까지 접근을 다시 map을 통해 goal의 isComplete를 수정했어야 했던 것이었다.
const reducer = (state, action) => {
let newState = [];
switch(action.type) {
case 'TOGGLECOMPLETE' : {
newState = state.map((it)=>{
if(it.id === action.id) {
return {
...it,
goal: it.goal.map((goalIt) => {
if(goalIt.goalId === action.goalId) {
return {
...goalIt,
isComplete : !goalIt.isComplete
};
}
return goalIt;
})
}
}
return it;
})
break;
}
default : return state;
}
return newState;
}
function App() {
// reducer dispatch - 완료 변경하기
const toggleComplete = (id, goalId) => {
dispatch({type: "TOGGLECOMPLETE", id, goalId})
}
헤더 날짜 선택 시 해당 월의 아이템만 조회하기
받아온 diaryList의 데이터를 선택한 월의 데이터로만 필터링하여야 한다.
const Home = () => {
// 받아온 diaryList를 선택한 월에 따라 관리할 State
const [data, setData] = useState([]);
useEffect(()=>{
// diaryList의 값이 있을 때 해당 월의 첫 날 ~ 마지막 날의 데이터 조회
if(diaryList.length >= 1) {
// curDate에 저장된 날짜의 년, 월, 1(일)
const firstDay = new Date(
curDate.getFullYear(),
curDate.getMonth(),
1
).getTime();
// curDate에 저장된 날짜에서 日을 0으로 하면 전날이 나오기 때문에 getMonth +1月을 하여
// 해달 월의 마지막 일을 선택할 수 있다.
// 그리고 시간을 지정해주지 않으면 0시00분으로 설정이 되기 때문에
// 23:59:59 와 같이 시간도 같이 지정해주어야 데이터를 제대로 조회할 수 있다.
const lastDay = new Date(
curDate.getFullYear(),
curDate.getMonth() + 1,
0,
23,
59,
59
).getTime();
// diaryList의 날짜가 fisrtDay 이상이고 last이하인 데이터만 필터링
setData(diaryList.filter((it)=> firstDay <= it.date && it.date <= lastDay))
}
// diaryList or curDate가 변경될 때마다 실행
},[diaryList, curDate])
return
// diaryList 대신 필터링한 data를 전달
<DiaryList diaryList={data} />
}
이렇게 하고 테스트를 해보면
위와 같이 < 버튼을 눌렀을 때 바로 날짜가 변하지 않고 두 번 눌러야만 변화가 되고, > 버튼을 눌렀을 땐 9월은 건너뛰어지는 걸 볼 수 있다. 이는 테스트하는 오늘 날짜가 7월 31일이라 그런 것이다. 6월과 9월은 30일까지밖에 없기 때문에 바로 적용이 되지 않는 것이다.
위에서 <, > 버튼을 눌렀을 때 월을 조정하는 함수를 수정하여야 한다.
// 왼쪽, 오른쪽 버튼 눌러 月 감소, 증가
const increaseMonth = () => {
// setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()+1, curDate.getDate()));
setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()+1, 1));
}
const decreaseMonth = () => {
// setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()-1, curDate.getDate()));
setCurDate(new Date(curDate.getFullYear(), curDate.getMonth()-1, 1));
}
date를 오늘 날짜의 일자가 아닌 오늘 날짜의 월의 1일로 설정하면 정상적으로 필터링이 된다.
최신/오래된순, 주제별 필터링
DiaryList를 관리할 것이기 때문에 해당 컴포넌트에 만들어준다.
필터링을 할 select가 여러개이기 때문에 DiaryList 외부에 컴포넌트로 분리하여 관리한다.
// select 컴포넌트
// value : 선택된 항목을 관리할 State
// onChange : 선택된 항목의 상태를 변경할 setState에 들어갈 값
// optList : 사용할 리스트
const ControlMenu = ({value, onChange, optList}) => {
return (
<select className = "ControlMenu"
value={value}
onChange={(e)=> onChange(e.target.value)}
>
{
optList.map((item, index)=> (
<option key={index} vlaue={item.name}>
{item.name}
</option>
))
}
</select>
)
}
// 사용할 optList
// 1. 최신 / 과거순
const sortOptList = [
{value : "newest", name : "최신순"},
{value : "oldest", name : "과거순"},
]
// 2. 주제별
const value = [
{value : "all", name : "전부"},
{value : "study", name : "공부"},
{value : "workout", name : "운동"},
{value : "daily", name : "일상"},
{value : "saving", name : "저축"},
]
const DiaryList = ({diaryList}) => {}
필터를 선택했을 때 필터링할 함수를 만들어 DiaryItem에 그 결과(필터링된 리스트)를 전달한다.
const DiaryList = ({diaryList}) => {
const navigate = useNavigate();
// 정렬 시 선택된 항목을 관리할 State
const [sortType, setSortType] = useState("newest");
const [subjectType, setSubjectType] = useState("all");
// 정렬 시 동작할 함수
const getProcessedDiaryList = () => {
// 날짜 정렬 비교함수 : 객체배열은 그냥 정렬이 안되고 비교함수를 만들어야 함
const compare = (a,b) => {
console.log(sortType)
if(sortType === "newest") {
return parseInt( b.date ) - parseInt( a.date )
} else {
return parseInt( a.date ) - parseInt( b.date )
}
}
// 배열을 sort 함수로 정렬 시 원본도 변하기 때문에 깊은 복사를하여 사용
const copyList = JSON.parse(JSON.stringify(diaryList));
// 정렬된 리스트
// 1. 주제 정렬
const filteredList = subjectType === "all" ? copyList : copyList.filter((item) => {
if(subjectType === "study") return item.subject === "study"
if(subjectType === "workout") return item.subject === "workout"
if(subjectType === "daily") return item.subject === "daily"
if(subjectType === "saving") return item.subject === "saving"
});
// 필터링된 리스트를 날짜별로 정렬
const sortedList = filteredList.sort(compare);
// 최종 필터링된 리스트를 반환
return sortedList;
}
return (
<div className="DiaryList">
<div className="menu_wrppaer">
<div className="menu_left_col">
<ControlMenu optList={sortOptList}
value={sortType}
onChange={setSortType}/>
<ControlMenu optList={subjectOptList}
value={subjectType}
onChange={setSubjectType}/>
</div>
<div className="menu_right_col">
<MyButton
text={ <img src={process.env.PUBLIC_URL + `assets/newDiary.png`}
style={{ width: "30px" }}/>}
onClick={()=> navigate('/new')} />
</div>
</div>
{getProcessedDiaryList().map((it) => (
<DiaryItem key={it.id} {...it} />
))}
</div>
);
};
* JSON.Stringfy를 통해 문자열로 변환하고 다시 JSON.parse를 사용하여 배열로 복호화 하는 이유
배열을 복사하여 사용할 때
const copyList = diaryList;
위처럼 복사하지 않는 이유는, JS에서 배열과 객체는 참조타입이기 때문이다.
참조타입은 변수에 값이 직접 저장되는 것이 아니라, 메모리에서 데이터가 저장된 위치(주소)를 참조하게 된다.
const array1 = [1, 2, 3];
const array2 = array1;
array2.push(4);
console.log(array1); // [1, 2, 3, 4]
예를 들어 위와 같이 array2를 복사하게 된다면, array2는 array1을 참고하고 있기 때문에 array2의 값을 바꿔도 array1 또한 값이 변경되게 된다. 그렇기 때문에 배열을 복사할 때에는 단순히 copyArr = originalArr 이런 식으로 복사를 하는 것이 아니라, 새로운 배열 또는 객체를 만들어야 하기 때문에 'JSON.parse(JSON.stringify())' 와 같이 문자열로 변환후 다시 파싱하여 새롭게 만들어야 한다. 이렇게 함으로써 새로운 메모리 공간에 데이터가 저장되기 때문에 원본 배열과 완전히 독립적으로 동작하게 된다.
홈화면 완성!
'JS > React Project' 카테고리의 다른 글
📆 위클리 플래너 - 첫화면, 로그인 (0) | 2023.09.05 |
---|---|
🏃♀️ 목표 다이어리 - 글 작성 / 수정 (0) | 2023.08.07 |
🏃♀️ 목표 다이어리 만들기 - 기획 및 기초 세팅 (0) | 2023.07.28 |
📝 메모앱 업그레이드 (0) | 2023.07.25 |
📝 메모앱 최적화하기 (0) | 2023.07.22 |