본문 바로가기
  • 삽질하는 자의 블로그
메인-프로젝트/React - Do-Health 프로젝트

12. 달력 라이브러리 [Full-Calendar] 를 이용하여, 식단 적는 달력 만들기. [feat. 수많은 시행착오...]

by 이게뭐당가 2023. 1. 4.

개인별로 식단을 달력에 적고, 그 달력을 확인하는 기능이 필요했다.

 

여러가지 달력 라이브러리들을 찾아본 결과 Full-Calendar 가 가장 쓰기 좋고, 편해보여 가져와보았다.

 

1. 달력 라이브러리 : 풀 캘린더 ( Full-Calendar )

 

리액트 DOCS

 

Fullcalendar - Fullcalendar Examples - StackBlitz

Simple example projects for FullCalendar

stackblitz.com

타입스크립트 DOCS

 
리액트 DOCS : https://fullcalendar.io/docs/react
리액트 EXAMPLE : https://stackblitz.com/github/fullcalendar/fullcalendar-examples/tree/main/react?file=src%2Fevent-utils.js,src%2FDemoApp.jsx
타입스크립트 EXAMPLE : https://github.com/fullcalendar/fullcalendar-examples/tree/main/react-typescript/src

우선 타입스크립트의 DOCS 들을 전부 가져왔다.

DemoApp.jsx

index.css

event-util.js

 

 

2. 시작하자마자 에러발생

캘린더의 handleEventClick 의 함수에서 오류가 발생했다.

<오류>
    "confirm" 이 esLint 오류가 난다.

<해결>
    "window" 객체를 명시하여 해결했다.

<코드>
    handleEventClick = (clickInfo: EventClickArg) => {
        if (
            window.confirm(                     // window객체를 명시해주었다.
                `Are you sure you want to delete the event '${clickInfo.event.title}'`
            )
        ) {
            clickInfo.event.remove();
        }

 

 

3. DOCS 와 파일을 하나씩 뜯어보며, 데이터의 구조를 파악하고, DB에 어떻게 넣을지 고민했다.

[데이터 구성] -  "event-utils.js" 에서 확인

    let todayStr = new Date().toISOString().replace(/T.*$/, ""); // YYYY-MM-DD of today

    {
        id: createEventId(),        // 이건 고정시키고
        title: "All-day event",     // "타이틀"
        start: todayStr,            // 이건 시간
    },

[ 데이터 구성 추가 ] - 추후, email 을 이용해 데이터를 걸러낼것이므로

    {
        id: createEventId(),        // 이건 고정시키고
        title: "All-day event",     // "타이틀"
        start: todayStr,            // 이건 시간
        email: "test@test.com",     // 사용자 이메일을 추가시켜서 추후, 필터되게 만들자.
    },

 

4. 확인했다. 이제 캘린더에 값을 넣을때, fetch 를 활용하여 DB 에도 값을 넣어보자.

< helper / calendar-add-to-DB>

        export type PostType = {
            title: string;
            start: string;
            email: string;
        };

        export const addCalendarToDb = async (exerciseData: PostType) => {
            await fetch(
            "https://do-health-project-default-rtdb.firebaseio.com/user/calendar.json",
            {
                method: "POST",
                body: JSON.stringify(exerciseData),
                headers: {
                "Content-Type": "application/json",
                },
            }
            );
        };

<components / fullcalendar / calendar.tsx >

        import { addCalendarToDb } from "../helper/calendar-add-to-DB"; // 내가 넣은 DB에 넣을 함수
                        ...

        handleDateSelect = (selectInfo: DateSelectArg) => {
            let title = prompt("Please enter a new title for your event");
            let calendarApi = selectInfo.view.calendar;

            calendarApi.unselect(); // clear date selection
            console.log(selectInfo);

            if (title) {
                calendarApi.addEvent({		// 기존 달력의 입력함수
                    id: createEventId(),
                    title,
                    start: selectInfo.startStr,
                    end: selectInfo.endStr,
                    allDay: selectInfo.allDay,
                });

                addCalendarToDb({       //  [코드 추가] 내가 넣은 DB에 넣을 함수
                    title,
                    start: selectInfo.startStr,
                    email: "test@test.com",
                });
            }
    };

 

5. DB에 넣는 것도 성공했다. 이제 DB의 값을 가져와서 "리덕스 store" 에 넣어본다.

1) 캘린더용 슬라이스 생성

< store/calendar-slice.tsx >

    import { createSlice, PayloadAction } from "@reduxjs/toolkit";

    export interface PostCalendarType {
        title: string;
        start: string;
        email: string;
    }

    const initialState: { calendarData: PostCalendarType[] } = {
        calendarData: [],
    };

    const calendarSlice = createSlice({
        name: "calendar",
        initialState,
        reducers: {
            updateAllcalendar(state, action: PayloadAction<PostCalendarType[]>) {
            state.calendarData = action.payload; // payload 에서 오는 값을, calendarData 로 교체할 것이다.
            },
        },
    });

    export const calendarActions = calendarSlice.actions;

    export default calendarSlice.reducer;

 

2) 캘린더용 DB의 값 GET 하기 위한 Thunk(action) 생성

< store/calendar-actions.tsx >

    import { Dispatch } from "@reduxjs/toolkit";
    import { calendarActions } from "./calendar-slice";
    import { useDispatch } from "react-redux"; // useDispath 의 사전 생성
    import type { AppDispatch } from "../store/index"; //  action 생성자용 Dispatch 타입

    let eventGuid = 0;

    export function createEventId() {
        return String(eventGuid++);
    }

    export const sendRequest = () => {
        return async (dispatch: Dispatch) => {   // 타입은 Dispatch 이다.

            const fetchData = async () => {       // 비동기 함수 만들어서
                const response = await fetch(
                    "https://do-health-project-default-rtdb.firebaseio.com/user/calendar.json"
                );
                const responseData = await response.json();

                const refineData = [];              // 파이어 베이스에서 데이터 것 refine

                for (const key in responseData) {
                    refineData.push({
                        id: createEventId(),
                        title: responseData[key].start,
                        start: responseData[key].title,
                        email: responseData[key].email,
                    });
                }

                return refineData;
            };

            const allCalendar = await fetchData();

            dispatch(calendarActions.updateAllcalendar(allCalendar)); // inputData를 집어넣는 action 을 한다.
        };
    };

    //  Thunk action 생성자를 만들때, 타입을 지정하기
    //  https://redux.js.org/tutorials/typescript-quick-start#define-typed-hooks

    export const useAppDispatch: () => AppDispatch = useDispatch;

3) store / index.tsx

< store / index.tsx>

    import { configureStore } from "@reduxjs/toolkit";
    import calendarSlice from "./calendar-slice";

    export const store = configureStore({
        reducer: {
            calendar: calendarSlice,
        },
    });

    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;

 

 

6. 값도 가져왔다. 이제 넣어볼..까..? 어? Class 형 컴포넌트네

시련이 닥쳐왔다. Class형 컴포넌트... 잘 모른다... Google 을 뒤적여본다

 

** useSelector 는 리액트 훅이다. 즉, 리액트 컴포넌트 함수 안에서만 사용가능하고

   이 말인 즉슨, 클래스에서는 사용 할 수 없다

 

좋다! App.js 에서 받아와서, 캘린더로 넘겨주자.

 

열심히 구글링 해보니, 클래스형 컴포넌트에 props 에는 extend 를 이용한 상속으로 값을 넘겨준다.

아닐 수도 있다.

 

 

그래서 App.tsx 에서 Props 를 넘기고

클래스에서 받아온 props 의 타입을 지정하고, 사용해보았다.

< App.tsx >     // DB 값 받아온다.

    import { Route, Switch } from "react-router-dom";
    import { useEffect } from "react";

    import Calendar from "./fullcalendar/calendar";

    import { sendRequest as sendRequestForCalendars, useAppDispatch } from "./store/calendar-action";

    function App() {
        const calendarData = useSelector(    // 3. 받아온 DB의 캘린더 데이터를, App.tsx 에서 받아온다.
            (state: RootState) => state.calendar.calendarData
          );

        const dispatch = useAppDispatch();	// 1. 타입스크립트의 리덕스툴킷이므로, useAppDispatch() 지정한 타입 사용

        useEffect(() => {
            dispatch(sendRequestForCalendars());	// 2. DB 의 캘린더 값 받아오는 Thunk
        }, [dispatch]);

        return (
            <div className="App">
                <Switch>
                        ...
                    <Route path={"/calendar"}>
                        <Calendar calendarData={calendarData} />        // 4. 값 을 넘겨준다.
                    </Route>




< fullcalendar / calendar.tsx >  // 넘겨 받은 값을 사용해보자

                    ...

    interface DemoAppState {
        weekendsVisible: boolean;
        currentEvents: EventApi[];
    }

    interface Props {		
        calendarData: { title: string; start: string; email: string }[];    //1. Props type 지정후
    }

    export default class Calendar extends React.Component< Props, {}, DemoAppState> {    //2. Props 타입을 등록하고
        state: DemoAppState = {
            weekendsVisible: true,
            currentEvents: [],
        };

        render() {
            const { calendarData } = this.props;        //3. 값을 받아온다.

            return (
                <div className="demo-app">
                {this.renderSidebar()}
                <div className="demo-app-main">
                    <FullCalendar
                    plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}

                    ...

                    initialEvents={calendarData} // 4. 넣어봤는데, 잠깐 뜨고, 유지가 안된다.
                    select={this.handleDateSelect}

 

7. 이젠 Full-Calendar DOCS 를 읽어볼 차례다. 초기 값이 잠깐 뜨고 사라진다.

< 오류발생>
    데이터를 넣었지만, 잠깐 뜨고, 유지가 되지 않는다.

< 이유 >
    API 에서 강제로, INITIAL_EVETNT 를 초기값으로 고정시킨다.

처음에는, State 의 라이프사이클 때문인줄 알고, 콘솔을 찍어보았다.

 

아니었다. 무언가 강제로 Initial Calendar Data 를, 상단의 initailEvents 의 초기값으로 고정시킨다.

 

DOCS 를 뒤져보았다.

야호! 찾았다

 

넘겨 받은 값을 사용 할때에는, initialEvents 옵션이 아니라, events 옵션으로 변경해야한다.

 "alternatively, use the `events` setting to fetch from a feed"
 
 <div className="demo-app">
            {this.renderSidebar()}
            <div className="demo-app-main">
                <FullCalendar
                plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
                                ...

                events={data}           // [바로 여기를 event 로 교체] alternatively, use the `events` setting to fetch from a feed
                select={this.handleDateSelect}

 

성공적이다!

 

8. 최종 마무리. "로그인한 사람" 만 "email Id" 에 맞는 값을 필터하고, 그 값을 캘린더에 넣는다.

 

 

로그인 시에, 토큰을 쿠키에 저장했다.

또한 토큰과 함께, 추가적으로 로그인한 사람의 email 도 함께 쿠키에 저장했다.

const SignInForm = () => {
  const [cookies, setCookie] = useCookies(["auth-cookie"]);
				...
            
  const submitHanlder = async (e: FormEvent) => {
  			...
    const responseData = await signinHandler(email, password);

    if (responseData.idToken) {		 // idToken 이 있다면 OK
      setCookie("auth-cookie", {
        idToken: responseData.idToken,		// idToken 과
        email: responseData.email,		// email 을 전부 쿠키에 저장
      });

 

그러므로 내게는, 로그인한 사람의 email 이 있고

그 값을 활용 할 수 있다!

 

 

1) App.tsx 에서, 로그인 하지 않은 자를 쫓아낼, Getout 함수와, Data를 필터해서 넣어준다.

< App.tsx >

    import { useCookies } from "react-cookie";

    function App() {
        const [cookies] = useCookies(["auth-cookie"]);      // 로그인 쿠키
        const history = useHistory();

        const calendarData = useSelector(                   // 캘린더 값 넘겨받기
            (state: RootState) => state.calendar.calendarData
        );

        let filteredCalendar: PostCalendarType[] = [];      //캘린더 값 필터하기

        if (cookies["auth-cookie"]) {
            filteredCalendar = calendarData.filter(         // 로그인한 아이디로, 켈린더 데이터 뽑기
                (data) => data.email === cookies["auth-cookie"].email
            );
        }

        const getOut = () => {                              // 넘겨줄 getOut함수 만들기
            history.replace("/");
        };

        const dispatch = useAppDispatch();

        useEffect(() => {
            dispatch(sendRequestForCalendars());            // 캘린더 값 dispatch 함수
        }, [dispatch]);

        return (
            <div className="App">
                <Switch>
                    <Route path={"/calendar"}>
                        <Calendar
                            calendarData={filteredCalendar}
                            getOut={getOut}
                            isLogedIn={cookies["auth-cookie"]}    // 로그인 했는지 확인
                        />
                    </Route>

 

 

2) 캘린더에서, Getout 함수와, 캘린더 Data 를 받아 사용한다.

<components / fullcalendar / calendar.tsx >

    import { addCalendarToDb } from "../helper/calendar-add-to-DB"; // 내가 넣은 DB에 넣을 함수

                    ...

    interface Props {
        calendarData: { title: string; start: string; email: string }[];    // 넘겨받을 값 Type 지정
        getOut: () => void;		// 넘겨 받은 getOut 함수의 타입 지정
        isLogedIn: string;		// 넘겨 받은, 로그인확인 변수도 타입 지정
    }

    export default class Calendar extends React.Component<Props, {}, DemoAppState> {    // 타입 넣고
        state: DemoAppState = {
            weekendsVisible: true,
            currentEvents: [],
        };

        render() {
            const calendarData = this.props.calendarData;       // 뽑아서 사용
            const getOut = this.props.getOut;
            const isLogedIn = this.props.isLogedIn;

            if (!isLogedIn) {                           // 로그인 안하면 아웃
                getOut();
            }

            return (
                <div className="demo-app">
                    {this.renderSidebar()}
                    <div className="demo-app-main">
                        <FullCalendar
                                    ...
                            events={calendarData}       // 캘린더 데이터 넣기
                            select={this.handleDateSelect}



** [적절한 스타일링 추가, 필요없는 코드 제거] - 사이드바는 제거했다.

 

댓글