-
Notifications
You must be signed in to change notification settings - Fork 1
홈/멤버페이지/투두&일정생성 api 연결 #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, { id: string; title: string }[]>; | ||
| 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<string, ScheduleForCalendar[]>; | ||
| 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<Date | null>(null); | ||
| const [modalOpen, setModalOpen] = useState(false); | ||
| const [dailySchedules, setDailySchedules] = useState<DailySchedule[]>([]); // State for daily schedules | ||
| const [dailySchedulesLoading, setDailySchedulesLoading] = useState(false); // Loading state for daily schedules | ||
| const [dailySchedulesError, setDailySchedulesError] = useState<string | null>(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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential timezone issue with date formatting.
Consider using local date formatting: - const formattedDate = date.toISOString().split("T")[0];
+ const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;🤖 Prompt for AI Agents |
||
| 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 ( | ||
| <div className="w-full flex flex-col gap-2 border-b-[1.5px] border-main4"> | ||
| <CalendarHeader year={year} month={month} onPrev={handlePrev} onNext={handleNext} /> | ||
| <CalendarHeader | ||
| year={currentYear} | ||
| month={currentMonth} // Pass 0-indexed month | ||
| onPrev={handlePrev} | ||
| onNext={handleNext} | ||
| /> | ||
| <CalendarWeekdays /> | ||
| <CalendarGrid year={year} month={month} schedulesByDate={schedulesByDate} onSelect={handleDateSelect} /> | ||
| <CalendarGrid | ||
| year={currentYear} | ||
| month={currentMonth} // Pass 0-indexed month | ||
| schedulesByDate={schedulesByDate} // This is for day cell markers | ||
| onSelect={handleDateSelect} | ||
| /> | ||
|
|
||
| <CalendarModal | ||
| open={modalOpen} | ||
| date={selectedDate} | ||
| schedules={selectedSchedules} | ||
| schedules={dailySchedules} // Pass fetched daily schedules | ||
| loading={dailySchedulesLoading} // Pass loading state | ||
| error={dailySchedulesError} // Pass error state | ||
| onClose={() => setModalOpen(false)} | ||
| /> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,61 @@ | ||
| // 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; | ||
| } | ||
|
|
||
| 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 = <p className="text-sm text-gray4 text-center">일정 로딩 중...</p>; | ||
| } else if (error) { | ||
| content = <p className="text-sm text-red-500 text-center">{error}</p>; | ||
| } else if (schedules.length === 0) { | ||
| content = <p className="text-sm text-gray4 text-center">일정이 없습니다.</p>; | ||
| } else { | ||
| content = ( | ||
| <ul className="w-full flex flex-col gap-2"> | ||
| {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 ( | ||
| <li | ||
| key={`${s.title}-${index}`} // Use a combination for key as no unique ID for schedule item | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| className="flex items-center justify-between text-sm text-gray-800 bg-gray-100 px-2 py-1 rounded-[6px]" | ||
| > | ||
| <span className="text-left text-point w-1/4">{displayTime}</span> | ||
| <span className="text-center flex-1 truncate">{s.title}</span> | ||
| <span className="w-1/4"></span> {/* Placeholder for alignment */} | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
| ); | ||
| } | ||
|
|
||
| const hasSchedule = schedules.length > 0; | ||
|
|
||
| return ( | ||
| <> | ||
|
|
@@ -46,20 +82,7 @@ export default function CalendarModal({ | |
|
|
||
| {/* 내용 */} | ||
| <div className="flex flex-col items-center justify-center gap-2 flex-1"> | ||
| {hasSchedule ? ( | ||
| <ul className="w-full flex flex-col gap-2"> | ||
| {schedules.map((s) => ( | ||
| <li | ||
| key={s.id} | ||
| className="text-sm text-gray-800 bg-gray-100 px-2 py-1 rounded-[6px]" | ||
| > | ||
| {s.title} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ) : ( | ||
| <p className="text-sm text-gray4 text-center">일정이 없습니다.</p> | ||
| )} | ||
| {content} | ||
| </div> | ||
|
|
||
| {/* 확인 버튼 */} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,57 @@ | ||
| 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 ( | ||
| <section className="bg-white rounded-[20px] border-[1.5px] border-main3 pt-5 px-5 pb-2"> | ||
| <div className="flex items-center justify-between"> | ||
| <h1 className="text-[18px] font-bold mb-2 text-black1">To-Do</h1> | ||
| {isLeader && (<button onClick={() => navigate("/CreateToDo", { state: { studyId } })} className="px-2 py-1 bg-main4 rounded-[12px] items-center flex gap-1"> | ||
| <Plus className="text-point" /> | ||
| <span className="text-point text-[14px] font-medium translate-y-[1px] ">새로운 To-Do</span> | ||
| </button>)} | ||
| </div> | ||
| <p className="text-gray-500 text-center py-4">아직 To-Do가 없습니다.</p> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <section className="bg-white rounded-[20px] border-[1.5px] border-main3 pt-5 px-5 pb-2"> | ||
| <div className="flex items-center justify-between"> | ||
| <h1 className="text-[18px] font-bold mb-2 text-black1">To-Do</h1> | ||
| {isLeader && (<button onClick={() => navigate("/CreateToDo")} className="px-2 py-1 bg-main4 rounded-[12px] items-center flex gap-1"> | ||
| {isLeader && (<button onClick={() => navigate("/CreateToDo", { state: { studyId } })} className="px-2 py-1 bg-main4 rounded-[12px] items-center flex gap-1"> | ||
| <Plus className="text-point" /> | ||
| <span className="text-point text-[14px] font-medium translate-y-[1px] ">새로운 To-Do</span> | ||
| </button>)} | ||
| </div> | ||
|
|
||
| {todoGroups.map((group) => ( | ||
| {todos.map((group) => ( // Use todos prop instead of todoGroups mock data | ||
| <div key={group.id} className="space-y-1 pb-2"> | ||
| <span className="inline-block px-2 py-1 bg-main4 font-black1 text-[12px] font-medium rounded-[20px]"> | ||
| {group.groupName} | ||
| </span> | ||
| {group.todos.map((todo, index) => ( | ||
| <TodoItem | ||
| key={todo.id} | ||
| taskName={todo.taskName} | ||
| completedCount={todo.completedCount} | ||
| totalCount={todo.totalCount} | ||
| isChecked={todo.isChecked} | ||
| taskName={todo.name} // Use todo.name from API | ||
| completedCount={todo.completedParticipants} // Use todo.completedParticipants | ||
| totalCount={todo.totalParticipants} // Use todo.totalParticipants | ||
| isChecked={todo.isCompleted} // Use todo.isCompleted | ||
| isLast={index === group.todos.length - 1} | ||
| path={`/todo/${todo.id}`} | ||
| /> | ||
|
Comment on lines
48
to
57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential division by zero when The Consider handling this edge case either here or in <TodoItem
key={todo.id}
taskName={todo.name}
completedCount={todo.completedParticipants}
- totalCount={todo.totalParticipants}
+ totalCount={todo.totalParticipants || 1}
isChecked={todo.isCompleted}
isLast={index === group.todos.length - 1}
path={`/todo/${todo.id}`}
/>Or add validation in 🤖 Prompt for AI Agents |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type mismatch: setter props don't match updater function usage.
The prop types define simple value setters:
But
handlePrev/handleNext(lines 38, 41, 47, 51) call them with updater functions:Update the types to support both patterns using React's
Dispatch<SetStateAction<number>>:🤖 Prompt for AI Agents