diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 43df2ec..519d74c 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -1,62 +1,111 @@ -import { useState } from "react"; import CalendarHeader from "./CalendarHeader"; import CalendarWeekdays from "./Weekdays"; import CalendarGrid from "./CalendarGrid"; import CalendarModal from "./CalendarModal"; +import { useState } from "react"; +import type { ScheduleForCalendar, GetDailySchedulesResponse, DailySchedule } from "../../types/calender"; // Import new types +import { getDailySchedules } from "../../services/calendar.service"; // Import new service +import { AxiosError } from "axios"; // Import AxiosError -type CalendarProps = { - schedulesByDate: Record; - onSelect?: (date: Date) => void; -}; -export default function Calendar({ schedulesByDate, onSelect = () => {} }: CalendarProps ) { - const today = new Date(); - const [year, setYear] = useState(today.getFullYear()); - const [month, setMonth] = useState(today.getMonth()); +interface CalendarProps { + schedulesByDate: Record; + onSelect: (date: Date) => void; + currentYear: number; + currentMonth: number; // Now 0-indexed + setCurrentMonth: (month: number) => void; + setCurrentYear: (year: number) => void; + studyId: string; // New prop for studyId +} +export default function Calendar({ + schedulesByDate, + onSelect, + currentYear, + currentMonth, + setCurrentMonth, + setCurrentYear, + studyId, // Accept studyId +}: CalendarProps ) { const [selectedDate, setSelectedDate] = useState(null); const [modalOpen, setModalOpen] = useState(false); + const [dailySchedules, setDailySchedules] = useState([]); // State for daily schedules + const [dailySchedulesLoading, setDailySchedulesLoading] = useState(false); // Loading state for daily schedules + const [dailySchedulesError, setDailySchedulesError] = useState(null); // Error state for daily schedules const handlePrev = () => { - if (month === 0) { - setYear((y) => y - 1); - setMonth(11); + if (currentMonth === 0) { // 0월 (January) -> 이전 해 11월 (December) + setCurrentYear((y) => y - 1); + setCurrentMonth(11); } else { - setMonth((m) => m - 1); + setCurrentMonth((m) => m - 1); } }; const handleNext = () => { - if (month === 11) { - setYear((y) => y + 1); - setMonth(0); - } else { - setMonth((m) => m + 1); + if (currentMonth === 11) { // 11월 (December) -> 다음 해 0월 (January) + setCurrentYear((y) => y + 1); + setCurrentMonth(0); + } + else { + setCurrentMonth((m) => m + 1); } }; - const handleDateSelect = (date: Date) => { + const handleDateSelect = async (date: Date) => { setSelectedDate(date); setModalOpen(true); - onSelect(date); + onSelect(date); // Call parent's onSelect + + // Fetch daily schedules + setDailySchedulesLoading(true); + setDailySchedulesError(null); + try { + const formattedDate = date.toISOString().split("T")[0]; // YYYY-MM-DD + const response = await getDailySchedules(studyId, formattedDate); + // Frontend should limit to 3 schedules + setDailySchedules(response.schedules.slice(0, 3)); + } catch (error) { + console.error("일별 일정 조회 실패:", error); + if (error instanceof AxiosError) { + setDailySchedulesError(error.response?.data?.message || "일별 일정을 불러오는 중 오류가 발생했습니다."); + } else { + setDailySchedulesError("일별 일정을 불러오는 중 알 수 없는 오류가 발생했습니다."); + } + setDailySchedules([]); // Clear schedules on error + } finally { + setDailySchedulesLoading(false); + } }; - // 일정 불러오기 - const selectedKey = selectedDate - ? selectedDate.toISOString().split("T")[0] - : ""; - const selectedSchedules = schedulesByDate[selectedKey] || []; + // No longer needed here as data is fetched on date select + // const selectedKey = selectedDate + // ? selectedDate.toISOString().split("T")[0] + // : ""; + // const selectedSchedules = schedulesByDate[selectedKey] || []; // This now comes from dailySchedules return (
- + - + setModalOpen(false)} />
diff --git a/src/components/Calendar/CalendarModal.tsx b/src/components/Calendar/CalendarModal.tsx index 08be4ee..0d70241 100644 --- a/src/components/Calendar/CalendarModal.tsx +++ b/src/components/Calendar/CalendarModal.tsx @@ -1,9 +1,11 @@ -// src/components/Calendar/CalendarScheduleModal.tsx +import type { DailySchedule } from "../../types/calender"; -interface CalendarScheduleModalProps { +interface CalendarModalProps { open: boolean; date: Date | null; - schedules?: { id: string; title: string }[]; + schedules: DailySchedule[]; // Updated to DailySchedule[] + loading: boolean; // Added loading prop + error: string | null; // Added error prop onClose: () => void; } @@ -11,15 +13,49 @@ export default function CalendarModal({ open, date, schedules = [], + loading, + error, onClose, -}: CalendarScheduleModalProps) { - if (!open || !date) return null; +}: CalendarModalProps) { + if (!open) return null; // Only check 'open', allow date to be null for initial render safety - const handleClose = () => onClose(); + const handleClose = () => { + onClose(); + }; - const dateLabel = `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일`; + const dateLabel = date ? `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일` : ''; + + let content; + + if (loading) { + content =

일정 로딩 중...

; + } else if (error) { + content =

{error}

; + } else if (schedules.length === 0) { + content =

일정이 없습니다.

; + } else { + content = ( +
    + {schedules.map((s, index) => { + // Extract HH:mm from "YYYY M DD HH:mm" + const timeMatch = s.scheduleTime ? s.scheduleTime.match(/(\d{1,2}:\d{2})/) : null; + const displayTime = timeMatch ? timeMatch[1] : ''; + + return ( +
  • + {displayTime} + {s.title} + {/* Placeholder for alignment */} +
  • + ); + })} +
+ ); + } - const hasSchedule = schedules.length > 0; return ( <> @@ -46,20 +82,7 @@ export default function CalendarModal({ {/* 내용 */}
- {hasSchedule ? ( -
    - {schedules.map((s) => ( -
  • - {s.title} -
  • - ))} -
- ) : ( -

일정이 없습니다.

- )} + {content}
{/* 확인 버튼 */} diff --git a/src/components/CreateScheduleToDo/DeadlineSelector.tsx b/src/components/CreateScheduleToDo/DeadlineSelector.tsx index d1e7552..c39a9f2 100644 --- a/src/components/CreateScheduleToDo/DeadlineSelector.tsx +++ b/src/components/CreateScheduleToDo/DeadlineSelector.tsx @@ -1,21 +1,52 @@ import NumberInputBox from './NumberInputBox'; +import React from 'react'; // Import React for React.ChangeEvent + +interface DeadlineSelectorProps { + month: number | string; + setMonth: React.Dispatch>; + day: number | string; + setDay: React.Dispatch>; + hour: number | string; + setHour: React.Dispatch>; + minute: number | string; + setMinute: React.Dispatch>; +} + +export default function DeadlineSelector({ + month, setMonth, + day, setDay, + hour, setHour, + minute, setMinute, +}: DeadlineSelectorProps) { + + const handleMonthChange = (e: React.ChangeEvent) => { + setMonth(e.target.value); + }; + const handleDayChange = (e: React.ChangeEvent) => { + setDay(e.target.value); + }; + const handleHourChange = (e: React.ChangeEvent) => { + setHour(e.target.value); + }; + const handleMinuteChange = (e: React.ChangeEvent) => { + setMinute(e.target.value); + }; -export default function DeadlineSelector() { return (
- + - +
- + : - +
까지
diff --git a/src/components/MemberHeader.tsx b/src/components/MemberHeader.tsx index b4fcb73..07e665e 100644 --- a/src/components/MemberHeader.tsx +++ b/src/components/MemberHeader.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from "react-router-dom"; import type { Member } from "../types/member"; import Right from "../assets/right.svg?react"; @@ -6,15 +5,15 @@ interface MemberHeaderProps { members: Member[]; // 전체 멤버 리스트 currentMembers: number; // 현재 인원 totalMembers: number; // 전체 인원 + onViewAllClick: () => void; // "전체보기" 클릭 시 호출될 콜백 함수 } -export default function GroupMemberButton({ +export default function MemberHeader({ members, currentMembers, totalMembers, + onViewAllClick, }: MemberHeaderProps) { - const navigate = useNavigate(); - const displayMembers = members.slice(0, 3); return ( @@ -36,7 +35,7 @@ export default function GroupMemberButton({
diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx index 2a633f2..3ddf314 100644 --- a/src/components/TodoList.tsx +++ b/src/components/TodoList.tsx @@ -1,24 +1,46 @@ import TodoItem from "./ToDoItem"; -import { todoGroups } from "../mock/scheduleTodoData"; +// import { todoGroups } from "../mock/scheduleTodoData"; // Remove mock data import import { useNavigate } from "react-router-dom"; import Plus from "../assets/plus.svg?react"; +import type { TodoGroup } from "../types/todo"; // Import TodoGroup type +// Add a type for the component props +interface TodoListProps { + studyId: string; + isLeader: boolean; + todos: TodoGroup[]; // Accept grouped todos as prop +} -export default function TodoList() { +export default function TodoList({ studyId, isLeader, todos }: TodoListProps) { // Destructure todos from props const navigate = useNavigate(); - const isLeader = true; + + // If no todos, display a message + if (todos.length === 0) { + return ( +
+
+

To-Do

+ {isLeader && ()} +
+

아직 To-Do가 없습니다.

+
+ ); + } return (

To-Do

- {isLeader && ()}
- {todoGroups.map((group) => ( + {todos.map((group) => ( // Use todos prop instead of todoGroups mock data
{group.groupName} @@ -26,10 +48,10 @@ export default function TodoList() { {group.todos.map((todo, index) => ( diff --git a/src/components/UpcomingSchedule.tsx b/src/components/UpcomingSchedule.tsx index 6c0181a..028da2f 100644 --- a/src/components/UpcomingSchedule.tsx +++ b/src/components/UpcomingSchedule.tsx @@ -1,24 +1,51 @@ -import { dummySchedules } from "../mock/scheduleTodoData"; import UpcomingScheduleItem from "./UpcomingScheduleItem"; +// import { dummySchedules } from "../mock/scheduleTodoData"; // Remove mock data import import { useNavigate } from "react-router-dom"; import Plus from "../assets/plus.svg?react"; +import type { ScheduleItem } from "../types/schedule"; // Import ScheduleItem type -export default function UpcomingSchedule() { +interface UpcomingScheduleProps { + isLeader: boolean; + studyId?: string; + schedules: ScheduleItem[]; // Accept schedules as prop +} + +export default function UpcomingSchedule({ isLeader, studyId, schedules }: UpcomingScheduleProps) { // Destructure schedules from props const navigate = useNavigate(); - const isLeader = true; + + // If no schedules, display a message + if (schedules.length === 0) { + return ( +
+
+

다가오는 일정

+ {isLeader && ()} +
+

아직 일정이 없습니다.

+
+ ); + } + return (

다가오는 일정

- {isLeader && ()}
- {dummySchedules.map((item, index) => ( - ( // Use schedules prop instead of dummySchedules mock data + ))}
diff --git a/src/pages/CreateSchedulePage.tsx b/src/pages/CreateSchedulePage.tsx index 17e2431..8319b4b 100644 --- a/src/pages/CreateSchedulePage.tsx +++ b/src/pages/CreateSchedulePage.tsx @@ -1,53 +1,116 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { createSchedule } from "../services/schedule.service"; +import type { CreateScheduleRequest } from "../types/schedule"; import Header from "../components/Header"; import LabeledInput from "../components/CreateScheduleToDo/LabeledInput"; -import DateRangeSelector from "../components/CreateScheduleToDo/DateRangeSelector"; -import TimeRangeSelector from "../components/CreateScheduleToDo/TimeRangeSelector"; -import DeadlineSelector from "../components/CreateScheduleToDo/DeadlineSelector"; +import NumberInputBox from "../components/CreateScheduleToDo/NumberInputBox"; import ActionButton from "../components/ActionButton"; import SuccessModal from "../components/SuccessModal"; export default function CreateSchedulePage() { const navigate = useNavigate(); - // 상태 관리 (입력창과 연결할 데이터) + const location = useLocation(); + const studyId = location.state?.studyId as string | undefined; + const [scheduleName, setScheduleName] = useState(""); const [place, setPlace] = useState(""); const [showModal, setShowModal] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const today = new Date(); + const [month, setMonth] = useState(today.getMonth() + 1); + const [day, setDay] = useState(today.getDate()); + + useEffect(() => { + if (!studyId) { + alert("잘못된 접근입니다. 스터디 그룹 페이지로 돌아갑니다."); + navigate("/GroupHome", { replace: true }); + } + }, [studyId, navigate]); + + const isDisabled = scheduleName.trim() === "" || loading; + + const formatStartDate = (): string | null => { + if (!month || !day) return null; + const year = new Date().getFullYear(); + const formattedMonth = String(month).padStart(2, '0'); + const formattedDay = String(day).padStart(2, '0'); + + const date = new Date(`${year}-${formattedMonth}-${formattedDay}`); + if (isNaN(date.getTime())) return null; - const isDisabled = scheduleName.trim() === ""; + return `${year}-${formattedMonth}-${formattedDay}`; + } - const handleCreateSchedule = () => { - if (isDisabled) return; + const handleCreateSchedule = async () => { + if (isDisabled || !studyId) return; - // 폼 데이터 서버 전송 로직 (추가 예정) - console.log("일정 생성 데이터 제출:", { scheduleName, place }); + setLoading(true); + setError(null); - // [수정] 모달 표시 - setShowModal(true); + const startDate = formatStartDate(); + if (!startDate) { + setError("유효한 날짜를 입력해주세요."); + setLoading(false); + return; + } + + const requestData: CreateScheduleRequest = { + title: scheduleName, + startDate: startDate, + location: place || undefined, + }; + + try { + await createSchedule(studyId, requestData); + setShowModal(true); + } catch (err: any) { + console.error("Error creating schedule:", err); + setError(err.response?.data?.message || "일정 생성에 실패했습니다."); + } finally { + setLoading(false); + } }; const handleConfirm = () => { setShowModal(false); - navigate("/GroupHome"); // [요청] 홈 페이지로 연결 + navigate("/GroupHome", { state: { studyId } }); }; return (
-
+ setScheduleName(e.target.value)} /> - setPlace(e.target.value)} /> - - - + setPlace(e.target.value)} /> + + {/* Simplified Date Picker */} +
+ +
+ setMonth(e.target.value)} /> + + setDay(e.target.value)} /> + +
+
- + + {error &&

{error}

} + +
+ +
+ setShowModal(false)} @@ -57,6 +120,5 @@ export default function CreateSchedulePage() { message2="생성되었습니다." />
- ); } \ No newline at end of file diff --git a/src/pages/CreateToDoPage.tsx b/src/pages/CreateToDoPage.tsx index d4e4a9b..47c711f 100644 --- a/src/pages/CreateToDoPage.tsx +++ b/src/pages/CreateToDoPage.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; // Import useLocation import Header from "../components/Header"; import LabeledInput from "../components/CreateScheduleToDo/LabeledInput"; import DeadlineSelector from "../components/CreateScheduleToDo/DeadlineSelector"; @@ -10,12 +10,26 @@ import SuccessModal from "../components/SuccessModal"; import type { SelectableMember } from "../types/member"; import { dummyMembers } from "../mock/memberData"; +import { createTodo } from "../services/todo.service"; // Import createTodo +import type { CreateTodoRequest } from "../types/todo"; // Import types export default function CreateToDoPage() { const navigate = useNavigate(); - // 상태 관리 + const location = useLocation(); // Initialize useLocation + const studyId = location.state?.studyId as string | undefined; // Get studyId from state + + // State management const [scheduleName, setScheduleName] = useState(""); const [showModal, setShowModal] = useState(false); + const [loading, setLoading] = useState(false); // For API call loading state + const [error, setError] = useState(null); // For API call error state + + // State for DeadlineSelector + const today = new Date(); + const [month, setMonth] = useState(today.getMonth() + 1); + const [day, setDay] = useState(today.getDate()); + const [hour, setHour] = useState(today.getHours()); + const [minute, setMinute] = useState(today.getMinutes()); const [selectedMethods, setSelectedMethods] = useState({ file: false, @@ -24,11 +38,17 @@ export default function CreateToDoPage() { const [members, setMembers] = useState([]); - useEffect(() => { // 더미 멤버 상태 초기화 + useEffect(() => { + // If no studyId, redirect to group home + if (!studyId) { + navigate("/GroupHome", { replace: true }); + return; + } + // 더미 멤버 상태 초기화 setMembers(dummyMembers.map((m) => ({ ...m, checked: false }))); - }, []); + }, [studyId, navigate]); - const toggleMember = (id: number) => { + const toggleMember = (id: number) => { setMembers((prev) => prev.map((m) => m.id === id ? { ...m, checked: !m.checked } : m @@ -36,19 +56,83 @@ export default function CreateToDoPage() { ); }; - const isDisabled = scheduleName.trim() === ""; + // Helper function to format date + const formatDateTime = (): string | null => { + if (!month || !day || !hour || !minute) return null; + + const currentYear = new Date().getFullYear(); + // Pad single digits with leading zero + const formattedMonth = String(month).padStart(2, '0'); + const formattedDay = String(day).padStart(2, '0'); + const formattedHour = String(hour).padStart(2, '0'); + const formattedMinute = String(minute).padStart(2, '0'); + + // Basic validation for date parts (can be more robust) + if ( + isNaN(currentYear) || isNaN(parseInt(formattedMonth)) || isNaN(parseInt(formattedDay)) || + isNaN(parseInt(formattedHour)) || isNaN(parseInt(formattedMinute)) + ) { + return null; + } + + try { + const date = new Date(`${currentYear}-${formattedMonth}-${formattedDay}T${formattedHour}:${formattedMinute}:00`); + if (isNaN(date.getTime())) { + return null; // Invalid date + } + return date.toISOString().slice(0, 19); // Format to YYYY-MM-DDTHH:mm + } catch (e) { + console.error("Date formatting error:", e); + return null; + } + }; + + const isDisabled = scheduleName.trim() === "" || loading; - const handleCreateSchedule = () => { - if (isDisabled) return; - const selectedMembers = members.filter((m) => m.checked); - console.log("선택된 인증 방식:", selectedMethods); - console.log("선택된 멤버:", selectedMembers); - setShowModal(true); + const handleCreateSchedule = async () => { + if (isDisabled || !studyId) return; + + setLoading(true); + setError(null); + + const selectedParticipantIds = members.filter((m) => m.checked).map((m) => m.id); + const certificationTypes: ("TEXT_NOTE" | "FILE_UPLOAD")[] = []; + if (selectedMethods.text) { + certificationTypes.push("TEXT_NOTE"); + } + if (selectedMethods.file) { + certificationTypes.push("FILE_UPLOAD"); + } + + const dueDate = formatDateTime(); + + if (!dueDate) { + setError("유효한 마감 기한을 입력해주세요."); + setLoading(false); + return; + } + + const requestData: CreateTodoRequest = { + name: scheduleName, + dueDate: dueDate + ":00", // Append seconds to match YYYY-MM-DDTHH:mm:ss format + certificationTypes: certificationTypes, + participantIds: selectedParticipantIds, + }; + + try { + await createTodo(studyId, requestData); + setShowModal(true); + } catch (err: any) { + console.error("Error creating todo:", err); + setError(err.response?.data?.message || "To-Do 생성에 실패했습니다."); + } finally { + setLoading(false); + } }; const handleConfirm = () => { setShowModal(false); - navigate("/GroupHome"); // [요청] 홈 페이지로 연결 + navigate(`/GroupHome`, { state: { studyId } }); // Pass studyId back to GroupHome }; return ( @@ -56,12 +140,18 @@ export default function CreateToDoPage() {
setScheduleName(e.target.value)} /> - + + {error &&

{error}

} ([]); + const [groupedTodos, setGroupedTodos] = useState([]); + const [todosLoading, setTodosLoading] = useState(true); + const [todosError, setTodosError] = useState(null); + + const [schedules, setSchedules] = useState([]); // State for schedules + const [schedulesLoading, setSchedulesLoading] = useState(true); // Loading state for schedules + const [schedulesError, setSchedulesError] = useState(null); // Error state for schedules + + + const isLeader = members.some(member => member.isLeader); + useEffect(() => { // studyId가 없으면 그룹 페이지로 리다이렉트 if (!studyId) { - navigate("/Group"); + navigate("/Group", { replace: true }); return; } @@ -72,10 +89,80 @@ export default function GroupHomePage() { } }; + // To-Do 목록 조회 + const fetchTodos = async () => { + setTodosLoading(true); + setTodosError(null); + try { + const fetchedTodos = await getTodosByStudy(studyId); + setTodos(fetchedTodos); // Store raw todos + + // Group fetchedTodos by groupName + const grouped: { [key: string]: TodoGroup } = {}; + fetchedTodos.forEach(todoItem => { + if (!grouped[todoItem.groupName]) { + grouped[todoItem.groupName] = { + id: todoItem.groupId, // Assuming groupId is unique for a group + groupName: todoItem.groupName, + todos: [], + }; + } + grouped[todoItem.groupName].todos.push({ + id: todoItem.id, + name: todoItem.name, + description: todoItem.description, + dueDate: todoItem.dueDate, + certificationMethods: todoItem.certificationMethods, + isCompleted: todoItem.isCompleted, + completedParticipants: todoItem.completedParticipants, + totalParticipants: todoItem.totalParticipants, + // Map to old Todo properties if still used by TodoItem + taskName: todoItem.name, + completedCount: todoItem.completedParticipants, + totalCount: todoItem.totalParticipants, + isChecked: todoItem.isCompleted, + }); + }); + setGroupedTodos(Object.values(grouped)); + + } catch (error) { + console.error("To-Do 목록 조회 실패:", error); + if (error instanceof AxiosError) { + setTodosError(error.response?.data?.message || "To-Do 목록을 불러오는 중 오류가 발생했습니다."); + } else { + setTodosError("To-Do 목록을 불러오는 중 알 수 없는 오류가 발생했습니다."); + } + } finally { + setTodosLoading(false); + } + }; + + // 일정 목록 조회 + const fetchSchedules = async () => { + setSchedulesLoading(true); + setSchedulesError(null); + try { + const fetchedSchedules = await getSchedulesByStudy(studyId); + setSchedules(fetchedSchedules); + } catch (error) { + console.error("일정 목록 조회 실패:", error); + if (error instanceof AxiosError) { + setSchedulesError(error.response?.data?.message || "일정 목록을 불러오는 중 오류가 발생했습니다."); + } else { + setSchedulesError("일정 목록을 불러오는 중 알 수 없는 오류가 발생했습니다."); + } + } finally { + setSchedulesLoading(false); + } + }; + + fetchStudyDetail(); + fetchTodos(); + fetchSchedules(); // Call fetchSchedules }, [studyId, navigate]); - if (isLoading) { + if (isLoading || todosLoading || schedulesLoading) { // Update loading condition return (

로딩 중...

@@ -98,11 +185,14 @@ export default function GroupHomePage() { members={members} currentMembers={members.length} totalMembers={totalMembers} + onViewAllClick={() => navigate("/Member", { state: { studyId, members } })} />
{hasCurrentScheduling && } - - + {schedulesError &&

{schedulesError}

} + {!schedulesLoading && !schedulesError && } + {todosError &&

{todosError}

} + {!todosLoading && !todosError && }
); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 4562413..7be0510 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,8 +1,124 @@ +import { useState, useEffect } from "react"; import MyInfo from "../components/MyInfo"; import TodoList from "../components/TodoList"; import Calendar from "../components/Calendar/Calendar"; +import { getTodosByStudy } from "../services/todo.service"; // Import todo service +import type { GetTodosResponse, TodoGroup } from "../types/todo"; // Import todo types +import { getCalendarSummary } from "../services/calendar.service"; // Import calendar service +import type { GetCalendarSummaryResponse, ScheduleForCalendar } from "../types/calender"; // Import calendar types +import { AxiosError } from "axios"; // Import AxiosError for error handling + export default function HomePage() { + const [todos, setTodos] = useState([]); + const [groupedTodos, setGroupedTodos] = useState([]); + const [todosLoading, setTodosLoading] = useState(true); + const [todosError, setTodosError] = useState(null); + + const [calendarSchedulesByDate, setCalendarSchedulesByDate] = useState>({}); + const [calendarLoading, setCalendarLoading] = useState(true); + const [calendarError, setCalendarError] = useState(null); + + // For calendar month navigation + const today = new Date(); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [currentMonth, setCurrentMonth] = useState(today.getMonth()); // Month is 0-indexed + + // Temporary hardcoded studyId for HomePage + const tempStudyId = "mock-study-id-for-homepage"; // Use a UUID here in a real app + + useEffect(() => { + const fetchTodos = async () => { + setTodosLoading(true); + setTodosError(null); + try { + const fetchedTodos = await getTodosByStudy(tempStudyId); + setTodos(fetchedTodos); + + // Group fetchedTodos by groupName, similar to GroupHomePage + const grouped: { [key: string]: TodoGroup } = {}; + fetchedTodos.forEach(todoItem => { + if (!grouped[todoItem.groupName]) { + grouped[todoItem.groupName] = { + id: todoItem.groupId, + groupName: todoItem.groupName, + todos: [], + }; + } + grouped[todoItem.groupName].todos.push({ + id: todoItem.id, + name: todoItem.name, + description: todoItem.description, + dueDate: todoItem.dueDate, + certificationMethods: todoItem.certificationMethods, + isCompleted: todoItem.isCompleted, + completedParticipants: todoItem.completedParticipants, + totalParticipants: todoItem.totalParticipants, + taskName: todoItem.name, + completedCount: todoItem.completedParticipants, + totalCount: todoItem.totalParticipants, + isChecked: todoItem.isCompleted, + }); + }); + setGroupedTodos(Object.values(grouped)); + + } catch (error) { + console.error("HomePage To-Do 목록 조회 실패:", error); + if (error instanceof AxiosError) { + setTodosError(error.response?.data?.message || "To-Do 목록을 불러오는 중 오류가 발생했습니다."); + } else { + setTodosError("To-Do 목록을 불러오는 중 알 수 없는 오류가 발생했습니다."); + } + } finally { + setTodosLoading(false); + } + }; + + const fetchCalendarData = async () => { + setCalendarLoading(true); + setCalendarError(null); + try { + const calendarSummary = await getCalendarSummary(tempStudyId, currentYear, currentMonth + 1); // API expects 1-indexed month + const transformedSchedules: Record = {}; + calendarSummary.days.forEach(daySummary => { + transformedSchedules[daySummary.date] = daySummary.scheduleTitles.map((title, index) => ({ + id: `${daySummary.date}-${index}`, // Generate synthetic ID for key prop + title: title + })); + }); + setCalendarSchedulesByDate(transformedSchedules); + } catch (error) { + console.error("HomePage 달력 데이터 조회 실패:", error); + if (error instanceof AxiosError) { + setCalendarError(error.response?.data?.message || "달력 데이터를 불러오는 중 오류가 발생했습니다."); + } else { + setCalendarError("달력 데이터를 불러오는 중 알 수 없는 오류가 발생했습니다."); + } + } finally { + setCalendarLoading(false); + } + }; + + fetchTodos(); + fetchCalendarData(); + }, [currentYear, currentMonth]); // Depend on currentYear and currentMonth for calendar data + + if (todosLoading || calendarLoading) { + return ( +
+

로딩 중...

+
+ ); + } + + if (todosError || calendarError) { + return ( +
+

{todosError || calendarError}

+
+ ); + } + return (
@@ -10,31 +126,20 @@ export default function HomePage() {
console.log("선택된 날짜:", date)} + currentYear={currentYear} + currentMonth={currentMonth} // Pass 0-indexed month + setCurrentMonth={setCurrentMonth} + setCurrentYear={setCurrentYear} + studyId={tempStudyId} // Pass tempStudyId to Calendar />
{/* 🔥 UpcomingSchedule + TodoList (위쪽 12px, 좌우 16px 패딩) */}
- + {/* Pass fetched groupedTodos to TodoList */} +
diff --git a/src/pages/MemberPage.tsx b/src/pages/MemberPage.tsx index db9156c..fa30448 100644 --- a/src/pages/MemberPage.tsx +++ b/src/pages/MemberPage.tsx @@ -1,15 +1,35 @@ +import { useLocation } from "react-router-dom"; // useLocation 임포트 import Header from "../components/Header"; import MemberInfo from "../components/MemberInfo"; -import { dummyMembers } from "../mock/memberData"; +// import { dummyMembers } from "../mock/memberData"; // 더미 데이터 더 이상 필요 없음 import type { Member } from "../types/member"; export default function MemberPage() { + const location = useLocation(); + const members: Member[] | undefined = location.state?.members; + + if (!members || members.length === 0) { + return ( +
+

멤버 정보를 불러올 수 없습니다.

+
{/* 헤더는 계속 표시 */} +
+ ); + } + + // 스터디장을 맨 위로 오도록 정렬 + const sortedMembers = [...members].sort((a, b) => { + if (a.isLeader && !b.isLeader) return -1; // a가 리더, b가 리더 아님 -> a가 먼저 + if (!a.isLeader && b.isLeader) return 1; // a가 리더 아님, b가 리더 -> b가 먼저 + return 0; // 둘 다 리더이거나 둘 다 리더 아님 -> 순서 유지 + }); + return (
- {dummyMembers.map((member: Member) => ( + {sortedMembers.map((member: Member) => ( )) }
diff --git a/src/services/calendar.service.ts b/src/services/calendar.service.ts new file mode 100644 index 0000000..454e767 --- /dev/null +++ b/src/services/calendar.service.ts @@ -0,0 +1,17 @@ +// src/services/calendar.service.ts +import api from "./api"; +import type { GetCalendarSummaryResponse, GetDailySchedulesResponse } from "../types/calender"; + +export async function getCalendarSummary(studyId: string, year: number, month: number): Promise { + const response = await api.get(`/calendar`, { // Changed from /api/calendar + params: { studyId, year, month }, + }); + return response.data; +} + +export async function getDailySchedules(studyId: string, date: string): Promise { + const response = await api.get(`/calendar/${date}`, { // Changed from /api/calendar/${date} + params: { studyId }, + }); + return response.data; +} diff --git a/src/services/schedule.service.ts b/src/services/schedule.service.ts new file mode 100644 index 0000000..2bb1d05 --- /dev/null +++ b/src/services/schedule.service.ts @@ -0,0 +1,19 @@ +// src/services/schedule.service.ts +import api from "./api"; +import type { CreateScheduleRequest, CreateScheduleResponse, GetSchedulesResponse } from "../types/schedule"; + +export async function createSchedule( + studyId: string, + data: CreateScheduleRequest +): Promise { + const response = await api.post( + `/studies/${studyId}/schedules`, + data + ); + return response.data; +} + +export async function getSchedulesByStudy(studyId: string): Promise { + const response = await api.get(`/studies/${studyId}/schedules`); + return response.data; +} \ No newline at end of file diff --git a/src/services/todo.service.ts b/src/services/todo.service.ts new file mode 100644 index 0000000..0fd2076 --- /dev/null +++ b/src/services/todo.service.ts @@ -0,0 +1,19 @@ +// src/services/todo.service.ts +import api from "./api"; +import type { CreateTodoRequest, CreateTodoResponse, GetTodosResponse } from "../types/todo"; + +export async function createTodo( + studyId: string, + data: CreateTodoRequest +): Promise { + const response = await api.post( + `/studies/${studyId}/todos`, + data + ); + return response.data; +} + +export async function getTodosByStudy(studyId: string): Promise { + const response = await api.get(`/studies/${studyId}/todos`); + return response.data; +} \ No newline at end of file diff --git a/src/types/calender.ts b/src/types/calender.ts index a1fee0a..9a0f1ff 100644 --- a/src/types/calender.ts +++ b/src/types/calender.ts @@ -5,6 +5,30 @@ export type ScheduleForCalendar = { title: string; }; +export interface CalendarDaySummary { + date: string; // "YYYY-MM-DD" + scheduleTitles: string[]; +} + +export interface GetCalendarSummaryResponse { + year: number; + month: number; + days: CalendarDaySummary[]; +} + +export interface DailySchedule { + title: string; + location?: string; + scheduleTime: string; // e.g., "2024 1 15 14:00" + dDay: number; +} + +export interface GetDailySchedulesResponse { + date: string; // "YYYY-MM-DD" + schedules: DailySchedule[]; +} + + // "2025년 9월 19일 (금) 오후 00:00" → Date 객체로 변환 function parseKoreanDate(str: string): Date { return new Date( diff --git a/src/types/schedule.ts b/src/types/schedule.ts index e877462..a0070bb 100644 --- a/src/types/schedule.ts +++ b/src/types/schedule.ts @@ -1,11 +1,13 @@ export interface ScheduleItem { - id: number; - location: string; - scheduleName: string; - dateTime: string; - dDay: number; + id: string; // API 응답에 맞춰 string (UUID) + title: string; // 일정 이름 + location?: string; // 장소 (선택사항) + scheduleTime?: string; // YYYY M DD HH:mm 형식 (확정된 일정만) + dday?: number; // 오늘 기준 남은 일수 (확정된 일정만) } +export type GetSchedulesResponse = ScheduleItem[]; // GetSchedulesResponse는 ScheduleItem의 배열 + export interface TimeSlot { hour: number; minute: number; @@ -39,3 +41,15 @@ export interface ScheduleData { maxMembers: number; // 최대 인원 cells: ScheduleCell[]; // 모든 셀 데이터 } + +export interface CreateScheduleRequest { + title: string; + startDate: string; // "YYYY-MM-DD" + location?: string; +} + +export interface CreateScheduleResponse { + id: number; // The ID of the newly created schedule +} + + diff --git a/src/types/todo.ts b/src/types/todo.ts index 95dd3ac..25f6a32 100644 --- a/src/types/todo.ts +++ b/src/types/todo.ts @@ -8,25 +8,21 @@ export interface TodoMember { isMe?: boolean; // 현재 사용자인지 여부 } -export interface TodoData { - id: number; - groupName: string; - title: string; - completedCount: number; - totalCount: number; - dueDate: string; - progress: number; - completedMembers: TodoMember[]; - incompleteMembers: TodoMember[]; - isUserCompleted?: boolean; // 현재 사용자가 완료했는지 여부 -} - +// 기존 Todo 인터페이스를 API 응답에 맞춰 업데이트 export interface Todo { id: number; - taskName: string; - completedCount: number; - totalCount: number; - isChecked: boolean; + name: string; // API 응답의 '이름' 필드 + description?: string; // API 응답의 '설명' 필드 + dueDate: string; // API 응답의 '마감일' 필드 + certificationMethods: ("TEXT_NOTE" | "FILE_UPLOAD")[]; // API 응답의 '인증 방식' 필드 + isCompleted: boolean; // API 응답의 '완료 상태' 필드 (현재 사용자 기준 또는 전체) + completedParticipants: number; // API 응답의 '완료된 참여자 수' 필드 + totalParticipants: number; // API 응답의 '전체 참여자 수' 필드 + // 기존 ToDoItem에서 사용되던 필드 중 필요한 것들을 유지 + taskName?: string; // 기존 TodoItem과 호환성을 위해 유지 + completedCount?: number; // 기존 TodoItem과 호환성을 위해 유지 + totalCount?: number; // 기존 TodoItem과 호환성을 위해 유지 + isChecked?: boolean; // 기존 TodoItem과 호환성을 위해 유지 } export interface TodoGroup { @@ -34,3 +30,31 @@ export interface TodoGroup { groupName: string; todos: Todo[]; } + +export interface CreateTodoRequest { + name: string; + dueDate: string; // ISO 8601 format, e.g., "YYYY-MM-DDTHH:mm:ss" + certificationTypes: ("TEXT_NOTE" | "FILE_UPLOAD")[]; + participantIds: number[]; +} + +export interface CreateTodoResponse { + id: number; // Assuming the created todo has an ID +} + +// 새로운 API 응답을 위한 인터페이스 +export interface GetTodoItemResponse { + id: number; + name: string; + description?: string; + dueDate: string; + certificationMethods: ("TEXT_NOTE" | "FILE_UPLOAD")[]; + isCompleted: boolean; // 완료 상태 (현재 사용자 기준) + completedParticipants: number; + totalParticipants: number; + groupId: number; // 어떤 그룹에 속한 todo인지 식별하기 위해 추가 + groupName: string; // 그룹 이름을 직접 제공하면 유용 +} + +export type GetTodosResponse = GetTodoItemResponse[]; +