Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 77 additions & 28 deletions src/components/Calendar/Calendar.tsx
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;
Comment on lines +16 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type mismatch: setter props don't match updater function usage.

The prop types define simple value setters:

setCurrentMonth: (month: number) => void;
setCurrentYear: (year: number) => void;

But handlePrev/handleNext (lines 38, 41, 47, 51) call them with updater functions:

setCurrentYear((y) => y - 1);  // Passes a function, not a number

Update the types to support both patterns using React's Dispatch<SetStateAction<number>>:

+import type { Dispatch, SetStateAction } from "react";
+
 interface CalendarProps {
   schedulesByDate: Record<string, ScheduleForCalendar[]>;
   onSelect: (date: Date) => void;
   currentYear: number;
   currentMonth: number;
-  setCurrentMonth: (month: number) => void;
-  setCurrentYear: (year: number) => void;
+  setCurrentMonth: Dispatch<SetStateAction<number>>;
+  setCurrentYear: Dispatch<SetStateAction<number>>;
   studyId: string;
 }
🤖 Prompt for AI Agents
In src/components/Calendar/Calendar.tsx around lines 16-17, the prop types
declare setCurrentMonth and setCurrentYear as functions accepting a number, but
the component calls them with updater functions; change their types to accept
both a value or updater by using React.Dispatch<React.SetStateAction<number>>
(or import Dispatch and SetStateAction from React and use
Dispatch<SetStateAction<number>>), and update any imports accordingly so the
props type matches usage with functional updaters.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential timezone issue with date formatting.

toISOString() converts to UTC, which can cause date shifts for users in positive UTC offsets near midnight. For example, selecting January 2nd at 00:30 in JST (UTC+9) produces "2024-01-01".

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
In src/components/Calendar/Calendar.tsx around line 64, the code uses
date.toISOString().split("T")[0] which converts the date to UTC and can shift
the day for users in positive timezones; replace this with local-date
formatting—either use date.toLocaleDateString('en-CA') to get a YYYY-MM-DD
string in the user's local timezone or construct the string from
date.getFullYear(), (date.getMonth()+1) and date.getDate() with zero-padding to
ensure two-digit month/day.

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>
Expand Down
67 changes: 45 additions & 22 deletions src/components/Calendar/CalendarModal.tsx
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

li 요소의 keys.titleindex의 조합으로 생성하고 있습니다. 만약 같은 날짜에 동일한 제목의 일정이 여러 개 있다면 key가 중복될 수 있으며, 이는 React의 렌더링 오류를 유발할 수 있습니다. API 응답에서 각 일정(schedule)에 대한 고유 ID를 제공받아 key로 사용하는 것이 가장 이상적입니다.

백엔드 API를 수정하여 DailySchedule 객체에 고유한 id 필드를 포함하도록 요청하는 것을 권장합니다.

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 (
<>
Expand All @@ -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>

{/* 확인 버튼 */}
Expand Down
41 changes: 36 additions & 5 deletions src/components/CreateScheduleToDo/DeadlineSelector.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<number | string>>;
day: number | string;
setDay: React.Dispatch<React.SetStateAction<number | string>>;
hour: number | string;
setHour: React.Dispatch<React.SetStateAction<number | string>>;
minute: number | string;
setMinute: React.Dispatch<React.SetStateAction<number | string>>;
}

export default function DeadlineSelector({
month, setMonth,
day, setDay,
hour, setHour,
minute, setMinute,
}: DeadlineSelectorProps) {

const handleMonthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setMonth(e.target.value);
};
const handleDayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDay(e.target.value);
};
const handleHourChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHour(e.target.value);
};
const handleMinuteChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setMinute(e.target.value);
};

export default function DeadlineSelector() {
return (
<div className="flex flex-col gap-3 px-4 pt-5">
<label className="text-[16px] font-semibold text-black1">
완료 기한
</label>
<div className="flex items-center pb-4 border-b-[1.5px] border-main4 gap-2 text-[14px] font-medium text-black1">
<NumberInputBox />
<NumberInputBox value={month} onChange={handleMonthChange} />
<span>월</span>
<NumberInputBox />
<NumberInputBox value={day} onChange={handleDayChange} />
<span>일</span>
<div className="w-1" />
<div className="flex items-center gap-[12.5px]">
<NumberInputBox />
<NumberInputBox value={hour} onChange={handleHourChange} />
<span>:</span>
<NumberInputBox />
<NumberInputBox value={minute} onChange={handleMinuteChange} />
</div>
<span>까지</span>
</div>
Expand Down
9 changes: 4 additions & 5 deletions src/components/MemberHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useNavigate } from "react-router-dom";
import type { Member } from "../types/member";
import Right from "../assets/right.svg?react";

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 (
Expand All @@ -36,7 +35,7 @@ export default function GroupMemberButton({
</div>
</div>
<button
onClick={() => navigate("/Member")} className="text-[14px] text-point font-medium flex items-center">
onClick={onViewAllClick} className="text-[14px] text-point font-medium flex items-center">
전체보기
<Right />
</button>
Expand Down
40 changes: 31 additions & 9 deletions src/components/TodoList.tsx
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential division by zero when totalParticipants is 0.

The TodoItem component computes progress = (completedCount / totalCount) * 100. If todo.totalParticipants is 0, this results in NaN being displayed.

Consider handling this edge case either here or in TodoItem:

             <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 TodoItem to handle zero denominators.

🤖 Prompt for AI Agents
In src/components/TodoList.tsx around lines 48-57, the code passes
todo.totalParticipants directly which can be 0 and cause a division-by-zero/NaN
in TodoItem; fix by ensuring the values passed are safe: when
todo.totalParticipants is 0, pass a safe non-zero total (e.g. Math.max(1,
todo.totalParticipants)) and clamp completedCount to at most that total (e.g.
Math.min(todo.completedParticipants, safeTotal)); alternatively, move the guard
into TodoItem and compute progress there as totalCount > 0 ? (completedCount /
totalCount) * 100 : 0 so progress is 0 when denominator is zero.

Expand Down
Loading