[React] 구글 Keep만들기4

Jungmin Ji·2023년 8월 28일
0

구글 Keep 만들기

목록 보기
4/5
post-thumbnail

Note 메인 페이지 생성하기

UI

AllNotes.tsx

const AllNotes = () => {
  const dispatch = useAppDispatch();
  const { mainNotes } = useAppSelector((state) => state.notesList);
  const [filter, setFilter] = useState('');
  const [searchInput, setSearchInput]   = useState('');

  return (
    <Container>
      {mainNotes.length === 0 ? (
        <EmptyMsgBox>노트가 없습니다.</EmptyMsgBox>
      ) : (
        <>
          <TopBox>
            <InputBox>
              <input 
                type="text"
                value={searchInput}
                onChange={ (e) => setSearchInput(e.target.value)}
                placeholder="노트의 제목을 입력해주세요."
              />
            </InputBox>
            <div className='notes__filter-btn'>
              <ButtonOutline
                onClick={() => dispatch(toggleFiltersModal(true))}
                className='nav__btn'
              >
                <span>정렬</span>
              </ButtonOutline>
            </div>
          </TopBox>
          <Box>
            {/* Notes */}
          </Box>
        </>
      )}
    </Container>
  )
}

Note 데이터 생성하기

태그 이니셜데이터를 tagSlice.ts에 생성해놨었는데
노트도 들어가자마자 있을 수 있게 두개 생성하겠다.
src > notesData.ts

import { v4 } from "uuid";

export interface Note {
    title: string;
    content: string;
    tags: Tag[];
    color: string;
    priority: string;
    isPinned: boolean;
    isRead: boolean;
    date: string;
    createdTime: number;
    editedTime: null | number;
    id: string;
}

const notes = [
    {
        title: "Note 1 title",
        content: "Note 1 content",
        tags: [{ tag: "coding", id: v4() }],
        color: "#cce0ff",
        priority: "high",
        isPinned: true,
        isRead: false, 
        date: "10/12/22 2.55 PM",
        createdTime: new Date("Sat Dec 10  2022 14:55:22").getTime(),
        editedTime: null,
        id: v4()
    },
    {
        title: "Note 2 title",
        content: "Note 2 content",
        tags: [{ tag: "exercise", id: v4() }],
        color: "#ffcccc",
        priority: "high",
        isPinned: true,
        isRead: false, 
        date: "10/12/22 2.55 PM",
        createdTime: new Date("Sat Dec 10  2022 14:55:22").getTime(),
        editedTime: null,
        id: v4()
    }
]

export default notes;

이것을 mainNotes에 넣어야함
src > store > notesListSlice.ts

const initialState: NoteState = {
  mainNotes: [...notes],
  archiveNotes: [],
  trashNotes: [],
  editNote: null,
}

getAllNotes 유틸함수 작성

utils > getAllNotes.tsx

import { NoteCard } from "../components";
import { NotesContainer } from "../styles/styles";
import { Note } from "../types/note";

const getAllNotes = (mainNotes: Note[], filter: string) => {
    return (
        <>
            <div className="allNotes__notes-type">
                All Notes
            </div>
            <NotesContainer>
                {mainNotes.map((note) => (
                    <NoteCard key={note.id} note={note} type="notes" />
                ))}
            </NotesContainer>
        </>
    )
}

export default getAllNotes;

NoteCard 노트카드 컴포넌트 생성

components > NoteCard > NoteCard.tsx

import React from 'react'
import { Card, ContentBox, FooterBox, TagsBox, TopBox } from './NoteCard.styles';
import { NotesIconBox } from '../../styles/styles';
import { BsFillPinFill } from 'react-icons/bs';
import { Note } from '../../types/note';

interface NoteCardProps {
  note: Note,
  type: string
}
const NoteCard = ({ note, type }: NoteCardProps) => {
  const { title, content, tags, color, priority, date, isPinned, isRead, id } = note;

  return (
    <Card style={{ background: color }}>
      <TopBox>
        <div
          className='noteCard__title'>
            { title.length > 10 ? title.slice(0, 10) + "..." : title }
        </div>
        <div className='noteCArd__top-options'>
          <span className='noteCard__priority'>
            {priority}
          </span>
          {type !== "archive" && type !== "trash" && (
            <NotesIconBox
              className='noteCard__pin'
            >
              <BsFillPinFill 
                style={{ color: isPinned ? "red" : ""}}
              />
            </NotesIconBox>
          )}
        </div>
      </TopBox>
      <ContentBox>
        {content}
      </ContentBox>
      <TagsBox>
        {tags.map(({ tag, id }) => (
          <span key={id}>{tag}</span>
        ))}
      </TagsBox>
      <FooterBox>
        <div className='noteCard__date'>{date}</div>
      </FooterBox>
    </Card>
  )
}

export default NoteCard

Note 버튼 생성하기

edit, archive, trash 기능버튼
utils > getRelevantBtns.tsx

import { FaEdit, FaTrash, FaTrashRestore } from "react-icons/fa";
import { NotesIconBox } from "../styles/styles";
import { RiInboxUnarchiveFill } from 'react-icons/ri';
import { Note } from "../types/note";
import { Dispatch } from "@reduxjs/toolkit";


const getRelevantBtns = (type: string, note: Note, dispatch: Dispatch) => {
    if(type === "archive") {
        return (
            <>
                <NotesIconBox
                    onClick={() => dispatch(unArchiveNote(note))}
                    data-info="UnArchive"
                >
                    <RiInboxUnarchiveFill style={{ fontSize: '1rem' }} />
                </NotesIconBox>
                <NotesIconBox
                    onClick={() => dispatch(setTrashNotes(note))}
                    data-info="Delete"
                >
                    <FaTrash style={{ fontSize: '1rem' }} />
                </NotesIconBox>
            </>
        )
    } else if (type === "trash") {
        return (
          <>
            <NotesIconBox
              onClick={() => dispatch(restoreNote(note))}
              data-info="Restore"
            >
              <FaTrashRestore style={{ fontSize: "1rem" }} />
            </NotesIconBox>
            <NotesIconBox
              onClick={() => dispatch(deleteNote(note))}
              data-info="Delete"
            >
              <FaTrash style={{ fontSize: "1rem" }} />
            </NotesIconBox>
          </>
        );
    } else {
        return (
          <>
            <NotesIconBox
              onClick={clickHandler}
              data-info="Edit"
            >
              <FaEdit style={{ fontSize: "1rem" }} />
            </NotesIconBox>
            <NotesIconBox
              onClick={() => dispatch(setArchiveNotes(note))}
              data-info="Archive"
            >
              <FaTrashRestore style={{ fontSize: "1rem" }} />
            </NotesIconBox>
            <NotesIconBox
              onClick={() => dispatch(setTrashNotes(note))}
              data-info="Delete"
            >
              <FaTrash style={{ fontSize: "1rem" }} />
            </NotesIconBox>
          </>
        );
    }
}

export default getRelevantBtns;

getRelevantBtns 기능 생성하기

notesListSlice에서 reducer 기능생성

src > store > notesListSlice.ts

import { createSlice } from '@reduxjs/toolkit';
import { Note } from '../../types/note';
import notes from '../../notesData';

interface NoteState {
  mainNotes: Note[],
  archiveNotes: Note[],
  trashNotes: Note[],
  editNote: null | Note
}

const initialState: NoteState = {
  mainNotes: [...notes],
  archiveNotes: [],
  trashNotes: [],
  editNote: null,
}

enum noteType {
  mainNotes = 'mainNotes',
  archiveNotes = 'archiveNotes',
  trashNotes = 'trashNotes'
}

const notesListSlice = createSlice({
  name: "notesList",
  initialState,
  reducers: {
    setMainNotes: (state, { payload }) => { // note생성이나 수정 후 업데이트
      // 해당 note edit
      if(state.mainNotes.find(({id}) => id === payload.id)) { // id가 payload로 들어오는 새로운 노트의 id가 같다면 수정하는데
        state.mainNotes = state.mainNotes.map((note) => 
          note.id === payload.id ? payload : note) // 같은건 변경해주고 원래있는건 원래있는걸로 넣어줌
      }
      // note를 새롭게 생성
      else {
        state.mainNotes.push(payload)
      }
    },
    setTrashNotes: (state, { payload }) => {
      state.mainNotes = state.mainNotes.filter(({id})=> id !== payload.id); // payload와 다른것만 넣어줌. 
      state.archiveNotes = state.archiveNotes.filter(({id})=> id !== payload.id); // payload와 다른것만 넣어줌. 
      state.trashNotes.push({...payload, isPinned: false}); // 다른 property override해서  ... 풀어서 넣어줌
    },
    setArchiveNotes: (state, { payload }) => {
      state.mainNotes = state.mainNotes.filter(({ id }) => id !== payload.id);
      state.archiveNotes.push({...payload, isPinned: false});
    },
    unArchiveNote: (state, { payload }) => {
      state.archiveNotes = state.archiveNotes.filter(({id})=> id !== payload.id);
      state.mainNotes.push(payload);
    },
    restoreNote: (state, { payload }) => {
      state.trashNotes = state.trashNotes.filter(({id}) => id !== payload.id);
      state.mainNotes.push(payload);
    },
    deleteNote: (state, { payload }) => {
      state.trashNotes = state.trashNotes.filter(({ id }) => id !== payload.id);

    },
    setPinnedNotes: (state, { payload }) => {
      state.mainNotes = state.mainNotes.map((note) => 
        note.id === payload.id ? {...note, isPinned: !note.isPinned} : note); // isPinned 값 toggle
    },
    setEditNote: (state, { payload }) => {
      state.editNote = payload;
    },
    readNote: (state, { payload }) => {
      const {type, id} = payload;
      const setRead = (notes: noteType) => {
        state[notes] = state[notes].map((note: Note) => 
        note.id === id ? {...note, isRead: !note.isRead} : note)
      }

      if(type === "archive") {
        setRead(noteType.archiveNotes);
      } else if(type === "trash") {
        setRead(noteType.trashNotes);
      } else { // main, tag... 
        setRead(noteType.mainNotes);
      }
    },
    removeTags: (state, { payload }) => {
      state.mainNotes = state.mainNotes.map((note) => ({
        ...note, // spread operate
        tags: note.tags.filter(({ tag }) => tag !== payload.tag), // payload에 같은 태그는 삭제하고 넣어준다
      }));
    },
  },
});

export const {
  setMainNotes,
  setArchiveNotes,
  setTrashNotes,
  unArchiveNote, 
  restoreNote, 
  deleteNote,
  setPinnedNotes,
  setEditNote,
  readNote,
  removeTags,
} = notesListSlice.actions;

export default notesListSlice.reducer

readNote기능은 archiveNotes 타입이거나, trashNotes, mainNotes 일때 각각 isRead를 toggle해주는 기능이다. true일때 modal이 떠서 조회가 가능하다.
enum noteType을 따로 생성해서 노트타입으로 각 노트리스트에서 해당 노트를 조회하도록 한다.

clickHandler 함수

	// modal
    const clickHandler = () => {
        dispatch(toggleCreateNoteModal(true));
        dispatch(setEditNote(note));
    };

CreateNoteModal 넣어주기

App.tsx에서 viewEditTagsModal와 같은 위치에 수정/생성용 모달을 넣는다.

...
const { viewCreateNoteModal } = useAppSelector((state) => state.modal);
...
{viewCreateNoteModal && <CreateNoteModal />}

Note 수정 기능 생성하기

pin기능


NoteCard.tsx에서 onClick에 setPinnedNotes 기능 (noteListSlice.ts에 정의)

          {type !== "archive" && type !== "trash" && (
            <NotesIconBox
              className='noteCard__pin'
              onClick={() => dispatch(setPinnedNotes({id}))}
            >
              <BsFillPinFill 
                style={{ color: isPinned ? "red" : ""}}
              />
            </NotesIconBox>
          )}

CreateNoteModal 수정/생성 모달


getRelevantBtns에서 edit눌렀을때 clickHandler가 동작하면서 모달띄운다.
이때 모달 생성하기
components > Modal > CreateNoteModal.tsx

...
const CreateNoteModal = () => {
  const dispatch = useAppDispatch();
  const { editNote } = useAppSelector((state) => state.notesList);
  const { viewAddTagsModal } = useAppSelector((state) => state.modal);

  const [noteTitle, setNoteTitle] = useState(editNote?.title || "");
  const [value, setValue] = useState(editNote?.content || "");
  const [addedTags, setAddedTags] = useState(editNote?.tags || []);
  const [noteColor, setNoteColor] = useState(editNote?.color || "");
  const [priority, setPriority] = useState(editNote?.priority || "low");

  const closeCreateNoteModal = () => {
    dispatch(toggleCreateNoteModal(false));
    dispatch(setEditNote(null));
  };

  const tagsHandler = (tag: string, type: string) => {
    // tag add, delete 기능
    const newTag = tag.toLowerCase();
    if(type === 'add') {
      setAddedTags((prev) => [...prev, {tag: newTag, id: v4()}])
      console.log("여기2");
    } else {
      setAddedTags(addedTags.filter(({tag}) => tag !== newTag));
      console.log("여기")
    }

  };
  return (
    <FixedContainer>
      {viewAddTagsModal && (
        <TagsModal type="add" addedTags={addedTags} handleTags={tagsHandler} />
      )}

      <Box>
        <TopBox>
          <div className="createNote__title">
            {editNote ? "노트 수정하기" : "노트 생성하기"}
          </div>
          <DeleteBox
            className="createNote__close-btn"
            onClick={closeCreateNoteModal}
          ></DeleteBox>
        </TopBox>
        <StyledInput
          type="text"
          value={noteTitle}
          name="title"
          placeholder="제목..."
          onChange={(e) => setNoteTitle(e.target.value)}
        />
        <div className="createNote__create-btn">
          <ButtonFill>
            {editNote ? (
              <span>저장하기</span>
            ) : (
              <>
                <FaPlus />
                <span>생성하기</span>
              </>
            )}
          </ButtonFill>
        </div>
        <AddedTagsBox>
          {addedTags.map(({ tag, id }) => (
            <div key={id}>
              <span className="createNote__Tag">{tag}</span>
              <span
                className="createNote__tag-remove"
                onClick={() => tagsHandler(tag, "remove")}
              >
                <FaTimes />
              </span>
            </div>
          ))}
        </AddedTagsBox>
        <OptionsBox>
          <ButtonOutline
            onClick={() =>
              dispatch(toggleTagsModal({ type: "add", view: "true" }))
            }
          >
            Add Tag
          </ButtonOutline>
          <div>
            <label>배경색 : </label>
            <select
              value={noteColor}
              id="color"
              onChange={(e) => setNoteColor(e.target.value)}
            >
              <option value="">White</option>
              <option value="#ffcccc">Red</option>
              <option value="#ccffcc">Green</option>
              <option value="#cce0ff">Blue</option>
              <option value="#ffffcc">Yellow</option>
            </select>
          </div>
          <div>
            <label>우선순위 : </label>
            <select
              value={priority}
              id="priority"
              onChange={(e) => setPriority(e.target.value)}
            >
              <option value="low">Low</option>
              <option value="high">High</option>
            </select>
          </div>
        </OptionsBox>
      </Box>
    </FixedContainer>
  );
};

export default CreateNoteModal;

Tag 수정 기능 생성하기


기존 default로 생성했던 태그들 또는 새로운 태그를 추가하거나 뺄 수 있다.
components > Modal > TagsModal.tsx

...
interface TagsModalProps {
  type: string,
  addedTags?: Tag[], // optional
  handleTags?: (tag: string, type: string) => void
}
const TagsModal = ({ type, addedTags, handleTags }: TagsModalProps) => { // add 또는 edit
  const dispatch = useAppDispatch();
  const { tagsList } = useAppSelector((state) => state.tags); // 처음에 만들어놓은 tag들
  const [inputText, setInputText] = useState('');

  const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if(!inputText) {return;}

    dispatch(addTags({tag: inputText.toLowerCase(), id: v4() }));
    setInputText('');
  }
  const deleteTagsHandler = (tag: string, id: string) => {
    dispatch(deleteTags(id));
    dispatch(removeTags({ tag }));
  }
  return (
    <FixedContainer>
      <Box>
        <div className="editTags__header">
          <div className="editTags__title">
            {type === "add" ? "ADD" : "Edit"} Tags
          </div>
          <DeleteBox
            className="editTags__close"
            onClick={() => dispatch(toggleTagsModal({ type, view: false }))}
          >
            <FaTimes />
          </DeleteBox>
        </div>
        <form onSubmit={submitHandler}>
          <StyledInput
            type="text"
            value={inputText}
            placeholder="new tag..."
            onChange={(e) => setInputText(e.target.value)}
          />
        </form>
        <TagsBox>
          {tagsList.map(({ tag, id }) => (
            <li key={id}>
              <div className="editTags__tag">{getStandardName(tag)}</div>
              {type === "edit" ? (
                <DeleteBox onClick={() => deleteTagsHandler(tag, id)}>
                  <FaTimes />
                </DeleteBox>
              ) : (
                <DeleteBox>
                  {addedTags?.find(
                    (addedTag: Tag) => addedTag.tag === tag.toLowerCase()
                  ) ? (
                    <FaMinus onClick={() => handleTags!(tag, "remove")} />
                    // type단언 해주기. edit아니고 add일때 분명히 undefined가 아닐것이기때문에
                    // null이나 undefined가 아니다라는 뜻
                  ): (
                    <FaPlus onClick={() => handleTags!(tag, "add")}/>
                  )}
                </DeleteBox>
              )}
            </li>
          ))}
        </TagsBox>
      </Box>
    </FixedContainer>
  );
}

export default TagsModal

에디터 생성하기 (React-Quill)


CreateNoteModal.tsx에서 제목 하단에 <TextEditor color={noteColor} value={value} setValue={setValue} /> import, 추가하고
src > components > TextEditor.tsx 부분 작성

import React from "react";
import { Container } from './TextEditor.styles';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

interface TextEditorProps {
  value: string,
  setValue: React.Dispatch<React.SetStateAction<string>>,
  color: string
}

const formats = [
  "bold",
  "italic",
  "underline",
  "strike",
  "list",

  "color",
  "background",

  "image",
  "blockquote",
  "code-block",
];

const modules = {
  toolbar: [
    [{ list: "ordered" }, { list: "bullet" }],
    [],
    ["italic", "underline", "strike"],
    [],
    [{ color: [] }, { background: [] }],
    [],
    ["image", "blockquote", "code-block"],
  ],
};

const TextEditor = ({ color, value, setValue }: TextEditorProps) => {

  return (
    <Container noteColor={color}>
      <ReactQuill
        formats={formats}
        modules={modules}
        theme="snow"
        value={value}
        onChange={setValue}
      />
    </Container>
  );
};

export default TextEditor

노트 저장 완료하기

CreateNoteModal.tsx에서 저장하기/생성하기 버튼에 이벤트핸들러 넣고

<ButtonFill onClick={createNoteHanlder}>
  {editNote ? (
   <span>저장하기</span>
   ) : (
     <>
     <FaPlus />
     <span>생성하기</span>
     </>
   )}
</ButtonFill>

그 핸들러 기능을 모달 return부분 상단에 작성해준다.
1. 먼저 유효성검사를 하고
2. Note의 Partial로(부분만 사용하는 유틸리티타입) 사용하는 note 데이터 생성
3. 수정일때는 원래 가지고있던 editNote데이터들과 새로 만들어주는 note를 넣고 setMainNotes에서 id가 payload.id와 같은 노트를 변경해주고 원래있는건 그대로 넣어줌
4. 새로 생성할때는 note객체를 만들어서 push한다.

  const createNoteHanlder = () => {
    // 유효성검사
    if(!noteTitle) {
      toast.error('타이틀을 작성해주세요.');
      return;
    } else if(value === "<p><br/></p>") {
      toast.error('내용을 작성해주세요.');
    }

    const date = dayjs().format("DD/MM/YYYY h:mm A");

    let note: Partial<Note> = {
      title: noteTitle,
      content: value, 
      tags: addedTags,
      color: noteColor,
      priority,
      editedTime: new Date().getTime()
    }

    if(editNote) { // 수정
      note = { ...editNote, ...note }
    } else {
      note = {
        ...note,
        date,
        createdTime: new Date().getTime(),
        editedTime: null,
        isPinned: false,
        isRead: false,
        id: v4()
      }
    }

    dispatch(setMainNotes(note));
    dispatch(toggleCreateNoteModal(false));
    dispatch(setEditNote(null));

  };
profile
FE DEV/디블리셔

0개의 댓글

관련 채용 정보