๐Ÿ—“๏ธ FullCalendar ์‚ฌ์šฉ

์–ธ์ง€ยท2024๋…„ 12์›” 14์ผ

๐Ÿ—“๏ธ ๋‹ฌ๋ ฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๐Ÿ“ FullCalendar ์ด๋ž€?

: JavaScript๋กœ ๋งŒ๋“ค์–ด์ง„ ๋‹ฌ๋ ฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ์จ month, week, day ๋ณ„๋กœ ๋‚˜๋ˆ„์–ด ์ผ์ •์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“ ๋‚ด๊ฐ€ FullCalendar๋กœ ์„ ํƒํ•œ ์ด์œ 

  1. ๊ทธ๋ƒฅ ๊ธฐ๋ณธ์ ์ธ ๋‹ฌ๋ ฅ ๋””์ž์ธ์ด ๋‚˜์™”์Œ ์ข‹๊ฒ ์—ˆ๊ณ 
  2. ์ด์ „ ๋‹ฌ, ๋‹ค์Œ ๋‹ฌ๋กœ ์ด๋™ํ–ˆ์„๋•Œ ์•„๋ฌด ๋ฌธ์ œ ์—†์—ˆ์Œ ์ข‹๊ฒ ์—ˆ๊ณ 
  3. ์ปค์Šคํ…€์œผ๋กœ CSS ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ–ˆ์Œ ์ข‹๊ฒ ์—ˆ๋‹ค.
    โžก๏ธ 3๊ฐ€์ง€๊ฐ€ ์ถฉ์กฑํ•ด์„œ fullcalendar๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

๐Ÿ“ ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ

โญ๏ธ ๋ชฉํ‘œ

1๏ธโƒฃ ๋‹ฌ๋ ฅ ๋„์šฐ๊ธฐ
2๏ธโƒฃ ์ด๋ฒคํŠธ ์œ ๋ฌด์— ๋”ฐ๋ผ ์ƒ์„ฑ/์กฐํšŒํŽ˜์ด์ง€๋กœ ์ด๋™
3๏ธโƒฃ ์ด๋ฒคํŠธ ์œ ๋ฌด์— ๋”ฐ๋ผ hover ์‹œ [+] ๋ฒ„ํŠผ ๋…ธ์ถœ

๐Ÿ“ ์„ค์น˜ํ•˜๊ธฐ

npm install @fullcalendar/react @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction

  • @fullcalendar/react : fullcalendar๋ฅผ react์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
  • @fullcalendar/core : ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ์ œ๊ณต
  • @fullcalendar/daygrid : ๋‹ฌ๋ ฅ ๋ชจ์–‘์˜ view๋ฅผ ์ œ๊ณต
  • @fullcalendar/interaction : fullcalendar ๋‚ด ์‚ฌ์šฉ์ž ๋™์ž‘์„ ์ฒ˜๋ฆฌ

1๏ธโƒฃ ๋‹ฌ๋ ฅ ๋„์šฐ๊ธฐ

MyCalendar.jsx ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ app.jsx์—์„œ ์„ ์–ธํ•ด์„œ ๋ถˆ๋Ÿฌ์™”์Šด๋‹ค
๋‹ฌ๋ ฅ์—๋Š” ์ผ์ •์ด ์žˆ์–ด์•ผํ•˜๋‹ˆ ๋”๋ฏธ ๊ฐ’์„ ์ž…๋ ฅํ•ด์คฌ์Šด๋‹ค

const events = [
	{ title: '๐Ÿ˜ก', date: '2024-12-17', },
	{ title: '๐Ÿ˜', date: '2024-12-19', },
	{ title: '๐Ÿ˜ณ', date: '2024-12-23', },
];

๐Ÿ“ dateClick, eventClick

<FullCalendar 
	defaultView="dayGridMonth" 
	plugins={[ dayGridPlugin, interactionPlugin]}
	events={events}
	dateClick={(info) => {
		alert(`๋‚ ์งœ ํด๋ฆญ๋จ: ${info.dateStr}`);
	}}
	eventClick={(info) => {
		alert(`์ด๋ฒคํŠธ ํด๋ฆญ๋จ: ${info.event.startStr}`);
	}}
/>
  • dateClick : ์บ˜๋ฆฐ๋”์˜ ๋นˆ ๋‚ ์งœ ์…€์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋จ (์ฃผ๋กœ ์ด๋ฒคํŠธ ์ƒ์„ฑ์— ์‚ฌ์šฉ๋จ)
    โžก๏ธ info.dateStr : ์„ ํƒํ•œ ๋‚ ์งœ๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. (ํ˜•์‹ : YYYY-MM-DD)
  • eventClick : ์‚ฌ์šฉ์ž๊ฐ€ ์บ˜๋ฆฐ๋”์— ํ‘œ์‹œ๋œ ์ด๋ฒคํŠธ๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋จ(์ด๋ฒคํŠธ ์ˆ˜์ •/์‚ญ์ œ์— ์‚ฌ์šฉ๋จ)
    โžก๏ธ info.event.startStr : ์„ ํƒํ•œ ์ด๋ฒคํŠธ ๋‚ ์งœ๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. (ํ˜•์‹ : YYYY-MM-DD)


2๏ธโƒฃ ์ด๋ฒคํŠธ ์œ ๋ฌด์— ๋”ฐ๋ผ ์ƒ์„ฑ/์กฐํšŒํŽ˜์ด์ง€๋กœ ์ด๋™

App.jsx

<Router>
	<Routes>
		<Route path="/" element={<MyCalendar />} />
		<Route path="/diaries/create/:date" element={<CreateDiary />} />
		<Route path="/diaries/view/:date" element={<ViewDiary />} />
	</Routes>
</Router>

๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' ์„ ์–ธ์ด ํ•„์š”ํ•˜๋‹ค

  • path : ๋ฃจํŠธ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•ด์ค€๋‹ค.
    โžก๏ธ /:date : ์ด๋™ํ•˜๊ธฐ์ „ ํŽ˜์ด์ง€์—์„œ date ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ ๊ฒฝ๋กœ์— ๋ณด์—ฌ์ค€๋‹ค.
  • element : ์ง€์ •ํ•ด์ค€ ๊ฒฝ๋กœ์— ๋ณด์—ฌ์ค„ ํŽ˜์ด์ง€(์ปดํฌ๋„ŒํŠธ)๋ฅผ ์ง€์ •ํ•ด์ค€๋‹ค.
MyCalendar.jsx

// ํŽ˜์ด์ง€ ์ด๋™ ํ•จ์ˆ˜
const navigate = useNavigate();

<FullCalendar 
	defaultView="dayGridMonth" 
	plugins={[ dayGridPlugin, interactionPlugin]}
	events={events}
	dateClick={(info) => {
		alert(`๋‚ ์งœ ํด๋ฆญ๋จ: ${info.dateStr}`);
		navigate(`/diaries/create/${info.dateStr}`)
	}}
	eventClick={(info) => {
		alert(`์ด๋ฒคํŠธ ํด๋ฆญ๋จ: ${info.event.startStr}`);
		navigate(`/diaries/view/${info.event.startStr}`)
	}}
/>

dateClick์„ ํ–ˆ์„ ๊ฒฝ์šฐ ์ด๋™ํ•  ์ปดํฌ๋„ŒํŠธ์™€ eventClick์„ ํ–ˆ์„ ๊ฒฝ์šฐ ์ด๋™ํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์ •ํ•ด์คฌ๋‹ค.
โžก๏ธ ์—ฌ๊ธฐ์„œ App.jsx์— ๊ฒฝ๋กœ์— ์žˆ๋Š” /:date์— ${info.dateStr}, ${info.event.startStr} ์ด ๋“ค์–ด๊ฐ„๋‹ค.

โ—๏ธ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ ๋ฐœ์ƒ

์ด๋ฒคํŠธ๊ฐ€ ์žˆ๋Š” ๋‚ ์งœ์˜ ์œ—๋ถ€๋ถ„์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ dateClick์ด ๋œ๋‹ค โžก๏ธ ์ผ๊ธฐ๋Š” ํ•˜๋ฃจ์— ํ•œ๋ฒˆ ์ž‘์„ฑํ•ด์•ผํ•˜๋ฏ€๋กœ dateClick์ด ๋˜๋ฉด ์•ˆ๋จ

โ—๏ธํ•ด๊ฒฐ๋ฐฉ๋ฒ•

์ผ์ •์ด ์žˆ๋Š” ๋‚ ์งœ๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ๋‚ด๊ฐ€ dateClickํ•œ ๋‚ ์งœ์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์กฐํšŒํŽ˜์ด์ง€๋ฅผ ์—†์œผ๋ฉด ์ƒ์„ฑํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด ๋œ๋‹ค.
const hasEvent = events.some((event) => event.start === info.dateStr);

  • some ๋ฉ”์„œ๋“œ : ๋ฐฐ์—ด์—์„œ ์กฐ๊ฑด์— ๋งž๋Š” ์š”์†Œ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ture๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    (๊ทผ๋ฐ ์กฐ๊ฑด์ด ํ•˜๋‚˜๋ฐ–์— ์—†์–ด์„œ ๋งž์œผ๋ฉด ๋งž๊ณ  ํ‹€๋ฆฌ๋ฉด ํ‹€๋ฆฌ๊ฒ ๋„ค.....)
<FullCalendar 
	defaultView="dayGridMonth" 
	plugins={[ dayGridPlugin, interactionPlugin]}
	events={events}
	dateClick={(info) => {
		alert(`๋‚ ์งœ ํด๋ฆญ๋จ: ${info.dateStr}`);
        
		const hasEvent = events.some((event) => event.start === info.dateStr);

		if(!hasEvent){
			navigate(`/diaries/create/${info.dateStr}`)
		}else{
			navigate(`/diaries/view/${info.dateStr}`)
		}
	}}
    
	eventClick={(info) => {
		alert(`์ด๋ฒคํŠธ ํด๋ฆญ๋จ: ${info.event.startStr}`);
		navigate(`/diaries/view/${info.event.startStr}`)
	}}
/>


3๏ธโƒฃ ์ด๋ฒคํŠธ ์œ ๋ฌด์— ๋”ฐ๋ผ hover ์‹œ [+] ๋ฒ„ํŠผ ๋…ธ์ถœ

fullcalendar๊ฐ€ ์ข‹์•˜๋˜ ์ด์œ ๋Š” ๋‚ด๊ฐ€ ์„ ํƒํ•œ ๋‚ ์งœ ๊ฐ’์„ ์ž๋™์œผ๋กœ YYYY-MM-DD ํ˜•์‹์œผ๋กœ ๊ฐ€๊ณตํ•ด์ค˜์„œ ์ข‹์•˜์ง€๋งŒ dayCellDidMount ๋ผ๋Š” ์‚ฌ์šฉ์ž ๋™์ž‘์— ๋‚ด๊ฐ€ ์ปค์Šคํ…€์„ ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋” ์ข‹์•˜๋˜ ๊ฒƒ ๊ฐ™๋‹ค.

1. ๋ฒ„ํŠผ ์ƒ์„ฑํ•˜๊ธฐ

: ์ด๋ฒคํŠธ๊ฐ€ ์—†์œผ๋ฉด [+]๋ฒ„ํŠผ ๋‚˜์˜ค๊ฒŒ ํ•˜๊ณ  ์žˆ์œผ๋ฉด ์•ˆ๋‚˜์˜ค๊ฒŒํ•ด์•ผํ•˜๋Š”๋ฐ ์ผ๋‹จ ๋ฒ„ํŠผ๋ถ€ํ„ฐ ๋งŒ๋“ค์—ˆ๋‹ค.

const createButton = document.createElement("button");
createButton.textContent = "+";

button์ด๋ผ๋Š” ํƒœ๊ทธ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ  ์ด ํƒœ๊ทธ ์•ˆ์— '+' ํ…์ŠคํŠธ๋ฅผ ๋„ฃ์—ˆ๋‹ค.

2. dayCellDidMount๋กœ hover ๋งŒ๋“ค๊ธฐ

โ—๏ธdayCellDidMount ์—๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์š” ์š”์†Œ

  • info.date : ๊ฐ€๊ณต๋˜์ง€์•Š์€ ๋‚ ์งœ ex) Tue Dec 17 2024 00:00:00 GMT+0000
  • info.el: ๋‚ ์งœ ์…€์˜ DOM ์š”์†Œ

dateClick์—๋Š” ๊ฐ€๊ณต๋œ date๊ฐ€ ์žˆ์ง€๋งŒ dayCellDidMount์— ์žˆ๋Š” date๋Š” ๊ฐ€๊ณต๋˜์–ด์žˆ์ง€์•Š์•„ ๋จผ์ € YYYY-MM-DD ํ˜•์‹์ด ๋˜๋„๋ก ๊ฐ€๊ณต์„ ์‹œ์ผœ์ค€๋‹ค.

const date = info.date.toLocaleDateString("en-CA");

dayCellDidMount์—์„œ ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜๋Š”๋ฐ ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

  • toISOString().split("T")[0] : UTC ๊ธฐ์ค€์œผ๋กœ ๋ถˆ๋Ÿฌ์™€์„œ ์ด๋ฒคํŠธ์žˆ๋Š” ํ•˜๋ฃจ๋ณด๋‹ค +1์ผ ๋˜์–ด ๋ณด์—ฌ์ง„๋‹ค.
  • toLocaleDateString("en-CA") : ๋กœ์ปฌ ์‹œ๊ฐ„๋Œ€ ๊ธฐ์ค€์œผ๋กœ ๋ถˆ๋Ÿฌ์™€ ์›ํ•˜๋Š” ๋‚ ์งœ์— ์ž˜ ๋ณด์—ฌ์ง„๋‹ค.

๋‘˜๋‹ค YYYY-MM-DD ํ˜•์‹์œผ๋กœ ๊ฐ€๊ณต๋œ ์ƒํƒœ์ด๋‹ค.


โ—๏ธ ์ด์ œ ์ค‘์š”ํ•œ hover ์„ค์ •ํ•˜๊ธฐ
: dayCellDidMount์—์„œ๋Š” hover๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ  addEventListener๋กœ mouseenter, mouseleave ๋กœ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

// ๋‚ ์งœ ์…€์— ๋งˆ์šฐ์Šค๊ฐ€ ์˜ฌ๋ผ๊ฐ”์„ ๋•Œ
info.el.addEventListener("mouseenter", () => {
	info.el.style.backgroundColor = "#ccc"; // ๋ฐฐ๊ฒฝ์ƒ‰ ๋ณ€๊ฒฝ
    
	if (!hasEvent){
		info.el.appendChild(createButton);
	}});

// ๋‚ ์งœ ์…€์—์„œ ๋งˆ์šฐ์Šค๊ฐ€ ๋– ๋‚ฌ์„ ๋•Œ
info.el.addEventListener("mouseleave", () => {
	info.el.style.backgroundColor = ""; // ์›๋ž˜ ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ๋ณต๊ตฌ

	info.el.removeChild(createButton);
});

โžก๏ธ ์ด๋ฒคํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ mouseenter ์‹œ createButton ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ์ด๋ฒคํŠธ๊ฐ€ ์žˆ๋“  ์—†๋“  ๋‚ ์งœ ์…€์—์„œ ๋ฒ—์–ด๋‚  ๊ฒฝ์šฐ mouseleave createButton ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง€์šด๋‹ค.

โค๏ธ ์ตœ์ข… ๊ฒฐ๊ณผ๋ฌผ

<FullCalendar 
	defaultView="dayGridMonth" 
	plugins={[ dayGridPlugin, interactionPlugin]}
	events={events}
	dateClick={(info) => {
		alert(`๋‚ ์งœ ํด๋ฆญ๋จ: ${info.dateStr}`);
                    
		const hasEvent = events.some((event) => event.start === info.dateStr);

	if(!hasEvent){
		navigate(`/diaries/create/${info.dateStr}`)
	}else{
		navigate(`/diaries/view/${info.dateStr}`)
	}
                    
}}
	eventClick={(info) => {
		alert(`์ด๋ฒคํŠธ ํด๋ฆญ๋จ: ${info.event.startStr}`);
		navigate(`/diaries/view/${info.event.startStr}`)
	}}

	// ๊ฐ ๋‚ ์งœ๋ณ„๋กœ ์ด๋ฒคํŠธ ์ถ”๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ - ๊ฐ ๋‚ ์งœ ์…€์ด ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰
	// dayCellDidMount ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์š”์†Œ
	// info.date : ๊ฐ€๊ณต๋˜์ง€์•Š์€ ๋‚ ์งœ ex) Tue Dec 17 2024 00:00:00 GMT+0000
	// info.el: ๋‚ ์งœ ์…€์˜ DOM ์š”์†Œ
	dayCellDidMount={(info)=>{
   
		// ๊ฐ€๊ณต๋˜์ง€์•Š์€ ๋‚ ์งœ ๊ฐ€๊ณต์‹œํ‚ค๊ธฐ
		const date = info.date.toLocaleDateString("en-CA");
		// ๋‚ ์งœ์™€ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ๋Š” ๋‚ ์งœ ๋น„๊ต -> ์žˆ์„ ๊ฒฝ์šฐ True, ์—†์„ ๊ฒฝ์šฐ Fasle
		const hasEvent = events.some((event) => event.start === date);

		// ๋‚ ์งœ ์…€์— ๋งˆ์šฐ์Šค๊ฐ€ ์˜ฌ๋ผ๊ฐ”์„ ๋•Œ
		info.el.addEventListener("mouseenter", () => {
		info.el.style.backgroundColor = "#ccc"; // ๋ฐฐ๊ฒฝ์ƒ‰ ๋ณ€๊ฒฝ

			if (!hasEvent){
				info.el.appendChild(createButton);
			}
		});

	// ๋‚ ์งœ ์…€์—์„œ ๋งˆ์šฐ์Šค๊ฐ€ ๋– ๋‚ฌ์„ ๋•Œ
		info.el.addEventListener("mouseleave", () => {
			info.el.style.backgroundColor = ""; // ์›๋ž˜ ๋ฐฐ๊ฒฝ์ƒ‰์œผ๋กœ ๋ณต๊ตฌ

			info.el.removeChild(createButton);
		});
	}}
/>


โŒ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜ โŒ

Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    at HTMLTableCellElement.<anonymous> (Calendar.jsx:81:29)

์ด๋ฒคํŠธ ์žˆ๋Š” ๋‚ ์งœ์—์„œ ์ด๋ฒคํŠธ ์—†๋Š” ๋‚ ์งœ๋กœ ๋งˆ์šฐ์Šค ์ง€๋‚˜๊ฐ€๋ฉด ์ด ์˜ค๋ฅ˜๊ฐ€ ๊ณ„์† ๋œจ๋Š”๋ฐ ์ด ์˜ค๋ฅ˜๊ฐ€ ์Œ“์ด๋ฉด ์•ˆ์ข‹๋‹ค๊ทธ๋ž˜์„œ ์—†์• ๊ณ ์‹ถ์€๋ฐ ์—†์•จ ๋ฐฉ๋ฒ•์„ ๋ชจ๋ฅด๊ฒ ๋‹ค.. ์ผ๋‹จ ๊ตฌํ˜„ํ•œ ๊ฑธ๋กœ ๋งŒ์กฑํ•˜์ง€๋งŒ ์–ธ์  ๊ฐ„ ๊ณ ์น˜๊ณ ๋งŒ๋‹ค ใ…กใ……ใ…ก

0๊ฐœ์˜ ๋Œ“๊ธ€