diff --git a/package-lock.json b/package-lock.json index 93806b5e..97ec04e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,16 @@ "name": "my_links_user_team", "version": "0.1.0", "dependencies": { + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/react": "^6.1.15", "moment": "2.30.1", "next": "14.2.14", "react": "^18", "react-datepicker": "^7.5.0", "react-dom": "^18", + "react-icons": "^5.3.0", "tailwind-merge": "^2.5.3" }, "devDependencies": { @@ -152,6 +157,40 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "node_modules/@fullcalendar/core": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", + "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz", + "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz", + "integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.15.tgz", + "integrity": "sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4483,6 +4522,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4674,6 +4722,14 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index f8dae6ae..25132897 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,16 @@ "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix && prettier --write ." }, "dependencies": { + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/interaction": "^6.1.15", + "@fullcalendar/react": "^6.1.15", "moment": "2.30.1", "next": "14.2.14", "react": "^18", "react-datepicker": "^7.5.0", "react-dom": "^18", + "react-icons": "^5.3.0", "tailwind-merge": "^2.5.3" }, "devDependencies": { diff --git a/src/app/admin/(block)/block-menu.tsx b/src/app/admin/(block)/block-menu.tsx index 473df958..442a4698 100644 --- a/src/app/admin/(block)/block-menu.tsx +++ b/src/app/admin/(block)/block-menu.tsx @@ -27,8 +27,8 @@ const BlockMenu = ({ isOpen, setIsOpen }: Props) => { ]; return ( -
-
+
+

블록 선택하기

-
-

일정 추가하기

-

- 입력하는 진행기간에 따라 -
- 전체 일정이 최근 날짜 순서로 자동 정렬됩니다. -

- -
- ); -} diff --git a/src/app/admin/(block)/calendar/components/add-schedule-form.tsx b/src/app/admin/(block)/calendar/components/add-schedule-form.tsx deleted file mode 100644 index 439a770d..00000000 --- a/src/app/admin/(block)/calendar/components/add-schedule-form.tsx +++ /dev/null @@ -1,361 +0,0 @@ -"use client"; - -import { useEffect, useState, useCallback } from "react"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; - -interface Schedule { - id?: number; - title: string; - url?: string; - dateStart: string; - dateEnd: string; -} - -interface CalendarBlock { - id?: number; - type: number; - sequence: number; - style: number; - schedule: Schedule[]; -} - -export default function AddScheduleForm() { - const [startDate, setStartDate] = useState(""); - const [startTime, setStartTime] = useState(""); - const [endDate, setEndDate] = useState(""); - const [endTime, setEndTime] = useState(""); - const [title, setTitle] = useState(""); - const [url, setUrl] = useState(""); - const [showStartTime, setShowStartTime] = useState(false); - const [showEndTime, setShowEndTime] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [calendarBlock, setCalendarBlock] = useState( - null, - ); - const router = useRouter(); - - const fetchCalendarBlock = useCallback(async () => { - try { - if (calendarBlock) { - console.log("캘린더 블록이 이미 존재합니다."); - return; - } - - const token = sessionStorage.getItem("token"); - if (!token) { - throw new Error("로그인이 필요합니다."); - } - - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/link/list`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - console.log("Fetched data:", data); - - const existingCalendarBlock = data.data.find( - (item: CalendarBlock) => item.type === 7, - ); - - if (existingCalendarBlock) { - setCalendarBlock(existingCalendarBlock); - console.log("Existing calendar (block) found:", existingCalendarBlock); - } else { - console.log("캘린더 블록이 없습니다."); - } - } catch (error) { - console.error("Error fetching calendar (block):", error); - setError("캘린더 블록 정보를 가져오는데 실패했습니다."); - } - }, [calendarBlock]); - - useEffect(() => { - if (!calendarBlock) { - fetchCalendarBlock(); - } - }, [calendarBlock, fetchCalendarBlock]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(null); - - const schedule: Schedule = { - title, - url: url || undefined, - dateStart: `${startDate}T${startTime}:00.000Z`, - dateEnd: `${endDate}T${endTime}:00.000Z`, - }; - - try { - const token = sessionStorage.getItem("token"); - if (!token) { - throw new Error("로그인이 필요합니다."); - } - - console.log("Sending schedule data:", schedule); - - if (calendarBlock) { - console.log("Existing calendar (block) found:", calendarBlock); - const updatedSchedule = [...calendarBlock.schedule, schedule]; - - const updateResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/link/update`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - id: calendarBlock.id, - type: 7, - sequence: calendarBlock.sequence, - style: calendarBlock.style, - schedule: updatedSchedule, - }), - }, - ); - - if (!updateResponse.ok) { - const errorData = await updateResponse.json(); - throw new Error(errorData.message || "일정 추가에 실패했습니다."); - } - - alert("일정이 성공적으로 추가되었습니다."); - } else { - console.log("No calendar (block) found, creating a new one."); - - const newBlock: Omit = { - type: 7, - sequence: 8, - style: 1, - schedule: [schedule], - }; - - const createResponse = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/api/link/add`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(newBlock), - }, - ); - - if (!createResponse.ok) { - const errorData = await createResponse.json(); - throw new Error( - errorData.message || "캘린더 블록 생성에 실패했습니다.", - ); - } - - alert("캘린더 블록과 일정이 성공적으로 추가되었습니다."); - } - - await fetchCalendarBlock(); - router.push("/calendar"); - } catch (error) { - console.error("Error adding schedule:", error); - setError( - error instanceof Error - ? error.message - : "알 수 없는 오류가 발생했습니다.", - ); - } finally { - setIsLoading(false); - } - }; - - const timeOptions = Array.from( - { length: 24 }, - (_, i) => `${i.toString().padStart(2, "0")}:00`, - ); - - return ( -
-
- -
- setStartDate(e.target.value)} - className={`min-w-[160px] flex-1 rounded-md p-2 ${ - startDate ? "border-[#FFCAB5] bg-[#FEF1E5]" : "border-gray-300" - }`} - required - /> -
-
- Clock -
- setStartTime(e.target.value)} - className="w-full appearance-none rounded-md border-0 bg-transparent p-2 focus:outline-none focus:ring-0" - placeholder="시간" - required - readOnly - /> -
setShowStartTime(!showStartTime)} - > - Open -
- {showStartTime && ( -
- {timeOptions.map((time, i) => ( -
{ - setStartTime(time); - setShowStartTime(false); - }} - > - {time} -
- ))} -
- )} -
-
-
- -
- -
- setEndDate(e.target.value)} - className={`min-w-[120px] flex-1 rounded-md p-2 ${ - endDate ? "border-[#FFCAB5] bg-[#FEF1E5]" : "border-gray-300" - }`} - required - /> -
-
- Clock -
- setEndTime(e.target.value)} - className="w-full appearance-none rounded-md border-0 bg-transparent p-2 focus:outline-none focus:ring-0" - placeholder="시간" - required - readOnly - /> -
setShowEndTime(!showEndTime)} - > - Open -
- {showEndTime && ( -
- {timeOptions.map((time, i) => ( -
{ - setEndTime(time); - setShowEndTime(false); - }} - > - {time} -
- ))} -
- )} -
-
-
- -
- - setTitle(e.target.value)} - className="rounded-md border border-gray-300 p-2 text-sm placeholder-gray-300 focus:border-[#FFCAB5] focus:outline-none focus:ring-[#FFCAB5]" - placeholder="알리고 싶은 일정 내용이 잘 드러나면 좋아요" - required - /> -
- -
- - setUrl(e.target.value)} - className="rounded-md border border-gray-300 p-2 text-sm placeholder-gray-300 focus:border-[#FFCAB5] focus:outline-none focus:ring-[#FFCAB5]" - placeholder="일정에 관심 있을 때 이동시키고 싶은 링크가 있나요?" - /> -
- -
- -
- - {error &&

{error}

} -
- ); -} diff --git a/src/app/admin/(block)/calendar/components/calendar-header.tsx b/src/app/admin/(block)/calendar/components/calendar-header.tsx index 43ed3b8a..afcddde8 100644 --- a/src/app/admin/(block)/calendar/components/calendar-header.tsx +++ b/src/app/admin/(block)/calendar/components/calendar-header.tsx @@ -8,7 +8,7 @@ export default function CalendarHeader() { const router = useRouter(); const handleAddScheduleClick = () => { - router.push("calendar/add"); + router.push("calendar/manage"); }; return ( diff --git a/src/app/admin/(block)/calendar/components/calendar-view.tsx b/src/app/admin/(block)/calendar/components/calendar-view.tsx index 7a6a9d30..06bed072 100644 --- a/src/app/admin/(block)/calendar/components/calendar-view.tsx +++ b/src/app/admin/(block)/calendar/components/calendar-view.tsx @@ -1,158 +1,167 @@ -import React, { useState } from "react"; -import Image from "next/image"; +"use client"; -interface Schedule { - id: string; - title: string; - startDate: string; - endDate: string; -} +import React, { useState, useEffect, useCallback } from "react"; +import Image from "next/image"; +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import interactionPlugin from "@fullcalendar/interaction"; +import { EventContentArg, EventClickArg } from "@fullcalendar/core"; +import { Schedule } from "./types"; interface CalendarViewProps { schedules: Schedule[]; + hasUserSchedules: boolean; } -const CalendarView: React.FC = ({ schedules }) => { - const [currentMonth] = useState(new Date(2023, 0, 1)); - - const getDaysInMonth = (date: Date): Date[] => { - const year = date.getFullYear(); - const month = date.getMonth(); - const firstDay = new Date(year, month, 1); - const lastDay = new Date(year, month + 1, 0); +const CalendarView: React.FC = ({ + schedules, + hasUserSchedules, +}) => { + const initialDate = hasUserSchedules ? new Date() : new Date(2023, 0, 1); + const [currentMonth, setCurrentMonth] = useState(initialDate); + const calendarRef = React.useRef(null); + + const eventColors = [ + "#575757", + "#707070", + "#888888", + "#A0A0A0", + "#666666", + "#999999", + "#777777", + "#949494", + "#555555", + "#808080", + ]; + + const getBackgroundColor = (schedule: Schedule, index: number) => { + if (["1", "2", "3"].includes(String(schedule.id))) { + const startDay = new Date(schedule.startDate).getDate(); + const endDay = new Date(schedule.endDate).getDate(); + + if (startDay >= 2 && endDay <= 6) { + return "#575757"; + } else if (startDay >= 5 && endDay <= 10) { + return "#707070"; + } else { + return "#A0A0A0"; + } + } - const daysInMonth = []; - const totalDays = 35; + return eventColors[index % eventColors.length]; + }; - for (let i = 1 - firstDay.getDay(); daysInMonth.length < totalDays; i++) { - daysInMonth.push(new Date(year, month, i)); + const getEvents = useCallback(() => { + return schedules.map((schedule, index) => ({ + id: schedule.id, + title: schedule.title, + start: schedule.startDate, + end: schedule.endDate, + backgroundColor: getBackgroundColor(schedule, index), + borderColor: "transparent", + classNames: schedule.url + ? ["calendar-event", "cursor-pointer"] + : ["calendar-event", "cursor-default"], + extendedProps: { url: schedule.url }, + })); + }, [schedules]); + + useEffect(() => { + const calendarApi = calendarRef.current?.getApi(); + if (calendarApi) { + calendarApi.removeAllEvents(); + calendarApi.addEventSource(getEvents()); } - - return daysInMonth; + }, [getEvents]); + + const handlePrevMonth = () => { + const calendarApi = calendarRef.current?.getApi(); + calendarApi?.prev(); + const newDate = new Date(currentMonth); + newDate.setMonth(newDate.getMonth() - 1); + setCurrentMonth(newDate); }; - const toDate = (date: string | Date): Date => { - return new Date(new Date(date).setHours(0, 0, 0, 0)); + const handleNextMonth = () => { + const calendarApi = calendarRef.current?.getApi(); + calendarApi?.next(); + const newDate = new Date(currentMonth); + newDate.setMonth(newDate.getMonth() + 1); + setCurrentMonth(newDate); }; - const calculateOffsets = (start: Date, end: Date, weekStart: Date) => { - const startOffset = Math.max( - 0, - (start.getTime() - weekStart.getTime()) / (1000 * 3600 * 24), - ); - const endOffset = Math.min( - 6, - (end.getTime() - weekStart.getTime()) / (1000 * 3600 * 24), + const CustomToolbar = () => { + return ( +
+ +

+ {`${currentMonth.getFullYear()}.${String( + currentMonth.getMonth() + 1, + ).padStart(2, "0")}`} +

+ +
); - const duration = endOffset - startOffset + 1; - return { startOffset, duration }; }; - const getBackgroundColor = (start: Date, end: Date) => { - const startDay = start.getDate(); - const endDay = end.getDate(); - - if (startDay >= 2 && endDay <= 6) { - return "#575757"; - } else if (startDay >= 5 && endDay <= 10) { - return "#707070"; - } else { - return "#A0A0A0"; - } + const renderEventContent = (eventInfo: EventContentArg) => { + return ( +
+ {eventInfo.event.title} +
+ ); }; - const renderSchedules = (week: Date[]) => { - const weekStart = week[0]; - const weekEnd = week[6]; - return schedules - .filter((schedule) => { - const start = toDate(schedule.startDate); - const end = toDate(schedule.endDate); - return start <= weekEnd && end >= weekStart; - }) - .map((schedule, index) => { - const start = toDate(schedule.startDate); - const end = toDate(schedule.endDate); - const { startOffset, duration } = calculateOffsets( - start, - end, - weekStart, - ); - - return ( -
- {schedule.title} -
- ); - }); + const handleEventClick = (clickInfo: EventClickArg) => { + const url = clickInfo.event.extendedProps.url; + if (url) { + window.open(url, "_blank"); + } }; - const days = getDaysInMonth(currentMonth); - const weeks = Array.from({ length: 5 }, (_, i) => - days.slice(i * 7, (i + 1) * 7), - ); - return (
-
- Previous Month -

- {`${currentMonth.getFullYear()}.${String(currentMonth.getMonth() + 1).padStart(2, "0")}`} -

- Next Month +
+ { + const days = ["일", "월", "화", "수", "목", "금", "토"]; + return days[date.getDay()]; + }} />
-
- {["일", "월", "화", "수", "목", "금", "토"].map((day) => ( -
{day}
- ))} -
-
-
- {weeks.map((week, weekIndex) => ( - - {week.map((day) => ( -
- {day.getDate()} -
- ))} -
- {renderSchedules(week)} -
-
- ))} -
-
); }; diff --git a/src/app/admin/(block)/calendar/components/list-view.tsx b/src/app/admin/(block)/calendar/components/list-view.tsx index 2803424f..4070d77a 100644 --- a/src/app/admin/(block)/calendar/components/list-view.tsx +++ b/src/app/admin/(block)/calendar/components/list-view.tsx @@ -1,63 +1,117 @@ import React from "react"; - -interface Schedule { - id: string; - title: string; - startDate: string; - endDate: string; -} +import { Schedule } from "./types"; interface ListViewProps { schedules: Schedule[]; } +const formatDate = (dateString: string) => { + const date = new Date(dateString); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hour = date.getHours(); + const ampm = hour >= 12 ? "오후" : "오전"; + const hour12 = hour % 12 || 12; + return `${month}.${day} (${ampm} ${hour12}시)`; +}; + +const getScheduleStatus = (schedule: Schedule, index: number) => { + if (["1", "2", "3"].includes(String(schedule.id))) { + if (index === 0) { + return { + text: "OPEN", + color: "bg-[var(--primary)] text-white", + }; + } else if (index === 1) { + return { + text: "D-3", + color: "bg-gray-200 text-[var(--primary)]", + }; + } else { + return { + text: "SOON", + color: "bg-gray-200 text-[var(--primary)]", + }; + } + } + + const startDate = new Date(schedule.startDate); + const endDate = new Date(schedule.endDate); + const now = new Date(); + + if (now < startDate) { + const diffDays = Math.ceil( + (startDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + return { + text: `D-${diffDays}`, + color: "bg-gray-200 text-[var(--primary)]", + }; + } else if (now >= startDate && now <= endDate) { + return { + text: "OPEN", + color: "bg-[var(--primary)] text-white", + }; + } else { + return { + text: "CLOSED", + color: "bg-[#BABABA] text-[#eae9e9]", + }; + } +}; + const ListView: React.FC = ({ schedules }) => { + const handleClick = (url: string) => { + if (url) { + window.open(url, "_blank"); + } + }; + return (
-
-
- {schedules.map((schedule, index) => ( -
-
- - {index === 0 ? "OPEN" : index === 1 ? "D-3" : "SOON"} - -
-
-
-

- {new Date(schedule.startDate).toLocaleString("ko-KR", { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - })}{" "} - ~ - {new Date(schedule.endDate).toLocaleString("ko-KR", { - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - })} -

-

- {schedule.title} -

-
+
+
+
+ {schedules.map((schedule, index) => { + const status = getScheduleStatus(schedule, index); + const hasUrl = Boolean(schedule.url); + return ( +
hasUrl && handleClick(schedule.url!)} + > +
+ + {status.text} + +
+
+
+
+

+ {formatDate(schedule.startDate)} ~{" "} + {formatDate(schedule.endDate)} +

+

+ {schedule.title} +

+
+
+ ); + })}
- ))} +
); diff --git a/src/app/admin/(block)/calendar/components/schedule-list.tsx b/src/app/admin/(block)/calendar/components/schedule-list.tsx index 1c152acf..aa16f40a 100644 --- a/src/app/admin/(block)/calendar/components/schedule-list.tsx +++ b/src/app/admin/(block)/calendar/components/schedule-list.tsx @@ -1,5 +1,6 @@ import Image from "next/image"; -import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; interface Schedule { id: number; @@ -9,43 +10,13 @@ interface Schedule { dateEnd: string; } -const scheduleData: Schedule[] = [ - { - id: 1, - title: "일정 1", - url: "https://naver.com/", - dateStart: "2024-10-01T12:26:44.000Z", - dateEnd: "2024-10-02T12:26:44.000Z", - }, - { - id: 2, - title: "일정 2", - url: "https://google.com/", - dateStart: "2024-10-01T09:00:00.000Z", - dateEnd: "2024-11-30T18:00:00.000Z", - }, - { - id: 3, - title: "일정 3", - url: "https://github.com/", - dateStart: "2024-12-01T08:00:00.000Z", - dateEnd: "2024-12-31T17:00:00.000Z", - }, - { - id: 4, - title: "일정 4", - url: "https://microsoft.com/", - dateStart: "2025-01-01T10:00:00.000Z", - dateEnd: "2025-01-31T16:00:00.000Z", - }, - { - id: 5, - title: "일정 5", - url: "https://apple.com/", - dateStart: "2025-02-01T11:00:00.000Z", - dateEnd: "2025-02-28T15:00:00.000Z", - }, -]; +interface CalendarBlock { + id: number; + type: number; + sequence: number; + style: number; + schedule: Schedule[]; +} const formatDate = (dateString: string) => { const date = new Date(dateString); @@ -80,9 +51,41 @@ const getScheduleStatus = (schedule: Schedule) => { } }; -function ScheduleItem({ schedule }: { schedule: Schedule }) { +function EmptyState({ message }: { message: React.ReactNode }) { + return ( +
+ 빈 캘린더 +

{message}

+
+ ); +} + +function ScheduleItem({ + schedule, + onDelete, +}: { + schedule: Schedule; + onDelete: (id: number) => void; +}) { + const router = useRouter(); const status = getScheduleStatus(schedule); + const handleEdit = () => { + router.push(`/admin/calendar/manage?mode=edit&id=${schedule.id}`); + }; + + const handleClick = (url: string) => { + if (url) { + window.open(url, "_blank"); + } + }; + return (
@@ -91,17 +94,26 @@ function ScheduleItem({ schedule }: { schedule: Schedule }) { > {status.text}
-
+
schedule.url && handleClick(schedule.url)} + >
{formatDate(schedule.dateStart)} ~ {formatDate(schedule.dateEnd)}
-
{schedule.title}
+
{schedule.title}
- -
@@ -114,11 +126,134 @@ function ScheduleItem({ schedule }: { schedule: Schedule }) { export default function ScheduleList() { const [isOpen, setIsOpen] = useState(true); const [activeTab, setActiveTab] = useState<"current" | "past">("current"); + const [schedules, setSchedules] = useState([]); + const [calendarBlock, setCalendarBlock] = useState( + null, + ); + const [error, setError] = useState(null); + const toggleOpen = () => setIsOpen(!isOpen); + useEffect(() => { + fetchSchedules(); + }, []); + + const fetchSchedules = async () => { + setError(null); + try { + const token = sessionStorage.getItem("token"); + if (!token) { + throw new Error("로그인이 필요합니다."); + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/list`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.code === 200 && Array.isArray(data.data)) { + const foundCalendarBlock = data.data.find( + (item: CalendarBlock) => item.type === 7, + ); + + if (foundCalendarBlock) { + setCalendarBlock(foundCalendarBlock); + if (Array.isArray(foundCalendarBlock.schedule)) { + setSchedules(foundCalendarBlock.schedule); + } else { + setSchedules([]); + } + } else { + setSchedules([]); + } + } else { + throw new Error(`Unexpected data structure: ${JSON.stringify(data)}`); + } + } catch (err) { + console.error("Error fetching schedules:", err); + setError( + err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다.", + ); + } + }; + + const handleDelete = async (scheduleId: number) => { + try { + if (!calendarBlock) { + throw new Error("캘린더 블록을 찾을 수 없습니다."); + } + + const token = sessionStorage.getItem("token"); + if (!token) { + throw new Error("로그인이 필요합니다."); + } + + const updatedSchedules = schedules.filter( + (schedule) => schedule.id !== scheduleId, + ); + + const requestBody = { + id: calendarBlock.id, + type: 7, + sequence: calendarBlock.sequence, + style: calendarBlock.style, + schedule: updatedSchedules, + }; + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/update`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(requestBody), + }, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `Failed to delete schedule: ${JSON.stringify(errorData)}`, + ); + } + + const responseData = await response.json(); + + if (responseData.code === 200) { + setSchedules(updatedSchedules); + alert("일정이 성공적으로 삭제되었습니다."); + + window.location.reload(); + } else { + throw new Error( + `Failed to delete schedule. Server response: ${JSON.stringify(responseData)}`, + ); + } + } catch (error) { + console.error("Error deleting schedule:", error); + alert( + error instanceof Error + ? error.message + : "일정 삭제 중 오류가 발생했습니다.", + ); + } + }; + const currentDate = new Date(); - const filteredSchedules = scheduleData.filter((schedule) => { + const filteredSchedules = schedules.filter((schedule) => { const endDate = new Date(schedule.dateEnd); return activeTab === "current" ? endDate >= currentDate @@ -147,7 +282,7 @@ export default function ScheduleList() {
- {filteredSchedules.map((schedule) => ( - - ))} + {filteredSchedules.length > 0 ? ( + filteredSchedules.map((schedule) => ( + + )) + ) : ( + + 진행 중이거나 예정된 일정이 없습니다. +
+ 일정을 추가하여 많은 방문자에게 알려보세요. + + ) : ( + <> + 지난 일정이 없습니다. +
+ 일정을 추가하여 많은 방문자에게 알려보세요. + + ) + } + /> + )}
)} diff --git a/src/app/admin/(block)/calendar/components/style-setting.tsx b/src/app/admin/(block)/calendar/components/style-setting.tsx index 67d36356..df32c969 100644 --- a/src/app/admin/(block)/calendar/components/style-setting.tsx +++ b/src/app/admin/(block)/calendar/components/style-setting.tsx @@ -1,11 +1,28 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import Image from "next/image"; import CalendarView from "./calendar-view"; import ListView from "./list-view"; +import { Schedule } from "./types"; -const sampleSchedules = [ +interface ApiSchedule { + id: number; + title: string; + dateStart: string; + dateEnd: string; + url?: string; +} + +interface CalendarBlock { + id: number; + type: number; + sequence: number; + style: number; + schedule: ApiSchedule[]; +} + +const sampleSchedules: Schedule[] = [ { id: "1", title: "[SAMPLE] 첫 번째 일정 예시", @@ -29,9 +46,68 @@ const sampleSchedules = [ export default function StyleSetting() { const [isOpen, setIsOpen] = useState(true); const [activeView, setActiveView] = useState<"list" | "calendar">("list"); + const [schedules, setSchedules] = useState([]); + const [calendarBlock, setCalendarBlock] = useState( + null, + ); + const [hasUserSchedules, setHasUserSchedules] = useState(false); + + const fetchSchedules = async () => { + try { + const token = sessionStorage.getItem("token"); + if (!token) { + throw new Error("로그인이 필요합니다."); + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/list`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (data.code === 200 && Array.isArray(data.data)) { + const foundCalendarBlock = data.data.find( + (item: CalendarBlock) => item.type === 7, + ); + + if (foundCalendarBlock && Array.isArray(foundCalendarBlock.schedule)) { + setCalendarBlock(foundCalendarBlock); + const formattedSchedules: Schedule[] = + foundCalendarBlock.schedule.map( + (schedule: ApiSchedule): Schedule => ({ + id: String(schedule.id), + title: schedule.title, + startDate: schedule.dateStart, + endDate: schedule.dateEnd, + url: schedule.url, + }), + ); + setSchedules(formattedSchedules); + setHasUserSchedules(formattedSchedules.length > 0); + } + } + } catch (err) { + console.error("Error fetching schedules:", err); + } + }; + + useEffect(() => { + fetchSchedules(); + }, []); const toggleOpen = () => setIsOpen(!isOpen); + const currentSchedules = hasUserSchedules ? schedules : sampleSchedules; + return (
@@ -51,30 +127,35 @@ export default function StyleSetting() {
{isOpen && (
-
- - +
+
+ + +
{activeView === "list" ? ( - + ) : ( - + )}
)} diff --git a/src/app/admin/(block)/calendar/components/types.ts b/src/app/admin/(block)/calendar/components/types.ts new file mode 100644 index 00000000..f6de07d1 --- /dev/null +++ b/src/app/admin/(block)/calendar/components/types.ts @@ -0,0 +1,7 @@ +export interface Schedule { + id: string; + title: string; + startDate: string; + endDate: string; + url?: string; +} diff --git a/src/app/admin/(block)/calendar/manage/page.tsx b/src/app/admin/(block)/calendar/manage/page.tsx new file mode 100644 index 00000000..5b13410e --- /dev/null +++ b/src/app/admin/(block)/calendar/manage/page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Image from "next/image"; +import ScheduleForm from "../components/schedule-form"; + +interface Schedule { + id?: number; + title: string; + url?: string; + dateStart: string; + dateEnd: string; +} + +interface CalendarBlockData { + type: number; + id: number; + schedule: Schedule[]; +} + +export default function ScheduleManagementPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const mode = searchParams.get("mode") || "add"; + const [schedule, setSchedule] = useState(null); + const [calendarBlockId, setCalendarBlockId] = useState(null); + + const handleClose = () => { + router.push("/admin/calendar"); + }; + + useEffect(() => { + if (mode === "edit") { + const fetchSchedule = async () => { + const scheduleId = searchParams.get("id"); + + if (!scheduleId) { + alert("일정 ID가 필요합니다."); + router.push("/admin/calendar"); + return; + } + + try { + const token = sessionStorage.getItem("token"); + if (!token) throw new Error("로그인이 필요합니다."); + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/link/list`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!response.ok) + throw new Error("데이터를 불러오는데 실패했습니다."); + + const data = await response.json(); + if (data.code === 200 && Array.isArray(data.data)) { + const calendarBlock = data.data.find( + (block: CalendarBlockData) => block.type === 7, + ); + if (calendarBlock) { + const foundSchedule = calendarBlock.schedule.find( + (s: Schedule) => s.id === Number(scheduleId), + ); + if (foundSchedule) { + setSchedule(foundSchedule); + setCalendarBlockId(calendarBlock.id); + } + } + } + } catch (error) { + console.error("Error fetching schedule:", error); + alert("일정을 불러오는데 실패했습니다."); + router.push("/admin/calendar"); + } + }; + + fetchSchedule(); + } + }, [mode, router, searchParams]); + + return ( +
+
+ +
+

+ {mode === "edit" ? "일정 수정하기" : "일정 추가하기"} +

+

+ 입력하는 진행기간에 따라 +
+ 전체 일정이 최근 날짜 순서로 자동 정렬됩니다. +

+ +
+ ); +} diff --git a/src/app/admin/(block)/divider/components/divider-selector.tsx b/src/app/admin/(block)/divider/components/divider-selector.tsx index 0130fe03..5b545b03 100644 --- a/src/app/admin/(block)/divider/components/divider-selector.tsx +++ b/src/app/admin/(block)/divider/components/divider-selector.tsx @@ -41,7 +41,11 @@ export default function DividerSelector({ className={`flex h-16 w-16 items-center justify-center rounded-xl border ${ selected === divider.name ? "border-black" : "border-gray-300" }`} - onClick={() => onSelect(divider.name)} + onClick={(e) => { + e.preventDefault(); + onSelect(divider.name as DividerType); + }} + type="button" > {divider.icon} diff --git a/src/app/admin/(block)/event/components/calendar.tsx b/src/app/admin/(block)/event/components/calendar.tsx index 759a6fd4..d8acffea 100644 --- a/src/app/admin/(block)/event/components/calendar.tsx +++ b/src/app/admin/(block)/event/components/calendar.tsx @@ -24,16 +24,17 @@ export default function Calendar({ -
+
{/* 시작 날짜 및 시간 선택 */}
- + setStartDate(date)} dateFormat="yyyy.MM.dd" placeholderText="날짜 선택" - className="w-full rounded-lg border-2 p-2" + enableTabLoop={false} + className="w-full max-w-[180px] rounded-lg border-2 p-2" />
{/* 종료 날짜 및 시간 선택 */}
- + setEndDate(date)} dateFormat="yyyy.MM.dd" placeholderText="날짜 선택" minDate={startDate || undefined} - className="w-full rounded-lg border-2 p-2" + enableTabLoop={false} + className="w-full max-w-[180px] rounded-lg border-2 p-2" />
diff --git a/src/app/admin/(block)/event/components/event-form.tsx b/src/app/admin/(block)/event/components/event-form.tsx index 9e37b114..04e544aa 100644 --- a/src/app/admin/(block)/event/components/event-form.tsx +++ b/src/app/admin/(block)/event/components/event-form.tsx @@ -124,6 +124,7 @@ export default function EventForm() { placeholder="어떤 이벤트인지 설명을 입력해주세요" value={description} onChange={(e) => setDescription(e.target.value)} + maxLength={100} /> setEventGuide(e.target.value)} + maxLength={100} /> (""); + const [isExpanded, setIsExpanded] = useState(false); // 토글 on/off + const [isDescriptionOverflowing, setIsDescriptionOverflowing] = + useState(false); // 입력된 설명 텍스트 너비 판단 + const [isTitleOverflowing, setIsTitleOverflowing] = useState(false); // 입력된 타이틀 텍스트 너비 판단 + const descriptionRef = useRef(null); + const titleRef = useRef(null); const calculateTimeLeft = () => { if (endDate && endTime) { @@ -52,33 +60,90 @@ export default function EventPreview({ return () => clearInterval(timer); // 컴포넌트 언마운트 시 타이머 정리 }, [endDate, endTime]); + useEffect(() => { + if (descriptionRef.current) { + setIsDescriptionOverflowing( + descriptionRef.current.scrollWidth > descriptionRef.current.clientWidth, + ); + } + if (titleRef.current) { + setIsTitleOverflowing( + titleRef.current.scrollWidth > titleRef.current.clientWidth, + ); + } + if (isExpanded && !isDescriptionOverflowing && !isTitleOverflowing) { + setIsExpanded(false); + } + }, [description, title, isDescriptionOverflowing, isTitleOverflowing]); + const formatDateTime = (date: Date | null, time: Date | null) => { if (!date || !time) return ""; return `${date.toLocaleDateString("ko-KR", { year: "2-digit", month: "2-digit", day: "2-digit" }).replace(/\s/g, "")} ${time.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit", hour12: false })}`; }; + + const handleExpandToggle = () => { + if (isDescriptionOverflowing || isTitleOverflowing) { + setIsExpanded(!isExpanded); + } + }; return ( -
-
+
+

event

-
-

+

+

{title || "타이틀을 입력해주세요"}

-

+

{description || "이벤트 설명을 입력해주세요"}

-
+

{startDate && startTime && endDate && endTime ? `${formatDateTime(startDate, startTime)} ~ ${formatDateTime(endDate, endTime)}` : "날짜와 시간을 선택해주세요"}

-

{timeLeft}

+
+

{timeLeft}

+ +
diff --git a/src/app/admin/(block)/event/page.tsx b/src/app/admin/(block)/event/page.tsx index 87f7f888..58949eec 100644 --- a/src/app/admin/(block)/event/page.tsx +++ b/src/app/admin/(block)/event/page.tsx @@ -3,9 +3,5 @@ import Image from "next/image"; import EventForm from "./components/event-form"; export default function page() { - return ( - <> - - - ); + return ; } diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index c69d4b5d..509c8d28 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -34,8 +34,9 @@ interface Block { export default function Admin() { useEffect(() => { const token = sessionStorage.getItem("token"); + console.log(token); if (!token) { - window.history.back(); + // window.history.back(); } const setVisitor = async () => { try { @@ -123,20 +124,15 @@ export default function Admin() { }; const updateBlockOrder = () => { - const block01sequence = blocks[0].sequence; - const block02sequence = blocks[1].sequence; const params = { order: blocks, }; - // const block1 = blocks[0]; - // const block2 = blocks[1]; - // block1["sequence"] = block02sequence; - // block2["sequence"] = block01sequence; - // const params = { - // order: [block1, block2], - // }; postBlock("/api/link/update/order", params, router).then((res) => { - if (res) console.log(res); + if (res) { + console.log(res); + const { data } = res; + setBlocks(data); + } }); }; @@ -228,7 +224,10 @@ export default function Admin() { )}
-
diff --git a/src/config/block_types.ts b/src/config/block_types.ts index be466762..7786753e 100644 --- a/src/config/block_types.ts +++ b/src/config/block_types.ts @@ -39,7 +39,7 @@ export const blockTypes: BlockType[] = [ title: "구분선", text: "블록과 블록 사이를 구분할 수 있어요", icon: DIVIDE_ICON, - path: "/admin/divide", + path: "/admin/divider", }, { title: "동영상",