Skip to content

Commit aa3d698

Browse files
committed
Merge branch 'feat/#2'
2 parents 2ca5749 + 83a283b commit aa3d698

File tree

8 files changed

+191
-42
lines changed

8 files changed

+191
-42
lines changed

app/routes/home.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Route } from './+types/home';
33
import { ReviewStartModal } from 'features/review';
44
import { useMyReviews } from 'features/history';
55
import { TodayReview } from 'entities/review';
6+
import { Todo } from 'features/todo/todo';
67

78
export function meta({}: Route.MetaArgs) {
89
return [{ title: 'LOOP' }];
@@ -19,6 +20,7 @@ export default function Home() {
1920
console.log(todayReview, myReviews);
2021
return (
2122
<div className="flex flex-col h-full">
23+
<Todo />
2224
<div className="w-full px-30">
2325
{todayReview ? (
2426
<TodayReview steps={todayReview.steps} />

features/goals/model/useCreateGoal.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMutation } from '@apollo/client/react';
22
import { CREATE_GOAL_MUTATION } from '../api/createGoal.mutation';
3+
import { MY_GOALS_QUERY } from '../api/myGoals.query';
34

45
interface CreateGoalInput {
56
title: string;
@@ -26,7 +27,10 @@ export function useCreateGoal() {
2627
>(CREATE_GOAL_MUTATION);
2728

2829
const createGoal = (input: CreateGoalInput) =>
29-
mutate({ variables: { input } });
30+
mutate({
31+
variables: { input },
32+
refetchQueries: [{ query: MY_GOALS_QUERY }],
33+
});
3034

3135
return { createGoal, loading, error };
3236
}

features/goals/model/useMyGoals.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export interface MyGoal {
1818

1919
export function useMyGoals() {
2020
const { data, loading, error, refetch } = useQuery<MyGoalsResponse>(
21-
MY_GOALS_QUERY
21+
MY_GOALS_QUERY,
22+
{
23+
fetchPolicy: 'cache-and-network',
24+
}
2225
);
2326

2427
const myGoals: MyGoal[] =

features/goals/ui/AddGoalModal.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Input } from 'shared/ui/components/input';
1313
interface AddGoalModalProps {
1414
open: boolean;
1515
onOpenChange: (open: boolean) => void;
16-
onAddGoal: (title: string) => void;
16+
onAddGoal: (title: string) => void | Promise<void>;
1717
}
1818

1919
export function AddGoalModal({
@@ -22,20 +22,26 @@ export function AddGoalModal({
2222
onAddGoal,
2323
}: AddGoalModalProps) {
2424
const [goalText, setGoalText] = useState('');
25+
const [isSubmitting, setIsSubmitting] = useState(false);
2526

2627
useEffect(() => {
2728
if (open) {
2829
setGoalText('');
2930
}
3031
}, [open]);
3132

32-
const handleSubmit = (e: React.FormEvent) => {
33+
const handleSubmit = async (e: React.FormEvent) => {
3334
e.preventDefault();
3435
if (!goalText.trim()) return;
3536

36-
onAddGoal(goalText);
37-
setGoalText('');
38-
onOpenChange(false);
37+
setIsSubmitting(true);
38+
try {
39+
await onAddGoal(goalText);
40+
setGoalText('');
41+
onOpenChange(false);
42+
} finally {
43+
setIsSubmitting(false);
44+
}
3945
};
4046

4147
const handleCancel = () => {
@@ -81,9 +87,9 @@ export function AddGoalModal({
8187
<Button
8288
type="submit"
8389
className="h-12 flex-1 bg-main1 px-6 py-3 hover:bg-main1/90"
84-
disabled={!goalText.trim()}
90+
disabled={!goalText.trim() || isSubmitting}
8591
>
86-
만들기
92+
{isSubmitting ? '만드는 중...' : '만들기'}
8793
</Button>
8894
</DialogFooter>
8995
</form>

features/task/model/useCreateTask.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useMutation } from '@apollo/client/react';
22
import { CREATE_TASK_MUTATION } from '../api/createTask.mutation';
3+
import { TASK_QEURY } from '../api/task.query';
4+
import { MY_GOALS_QUERY } from 'features/goals/api/myGoals.query';
35
import type { TaskDTO } from 'entities/task/type';
46

57
interface CreateTaskInput {
68
goalId: string;
79
title: string;
8-
taskDate: string; // yyyy-mm-dd
10+
date: string; // yyyy-mm-dd
911
}
1012

1113
interface CreateTaskResponse {
@@ -19,7 +21,21 @@ export function useCreateTask() {
1921
>(CREATE_TASK_MUTATION);
2022

2123
const createTask = (input: CreateTaskInput) =>
22-
mutate({ variables: { input } });
24+
mutate({
25+
variables: { input },
26+
refetchQueries: [
27+
{ query: MY_GOALS_QUERY },
28+
{
29+
query: TASK_QEURY,
30+
variables: {
31+
filter: {
32+
startDate: input.date,
33+
endDate: input.date,
34+
},
35+
},
36+
},
37+
],
38+
});
2339

2440
return { createTask, loading, error };
2541
}

features/todo/todo.tsx

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { useMemo, useState } from 'react';
1+
import { useMemo, useRef, useState } from 'react';
22
import { Send } from 'lucide-react';
33
import { Button } from 'shared/ui/components/button';
4+
import { Input } from 'shared/ui/components/input';
45
import { useGoals } from 'shared/context/GoalsContext';
6+
import { useMyGoals } from 'features/goals/model/useMyGoals';
57
import { useTask } from 'features/task/model/useTask';
68
import { formatDateToYYYYMMDD } from 'shared/utils';
79
import { AddGoalModal } from 'features/goals/ui/AddGoalModal';
@@ -11,8 +13,14 @@ import { TodoItem } from 'features/todo/TodoItem';
1113
export const Todo = () => {
1214
const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());
1315
const [isAddGoalOpen, setIsAddGoalOpen] = useState(false);
16+
17+
// History 페이지(useMyReviews)와 동일하게 페이지에서 직접 myGoals 쿼리 호출
18+
const {
19+
myGoals,
20+
loading: goalsLoading,
21+
refetch: refetchGoals,
22+
} = useMyGoals();
1423
const {
15-
goals,
1624
addGoal: handleAddTodo,
1725
addTask: handleAddTask,
1826
toggleTask: handleToggleTask,
@@ -21,16 +29,42 @@ export const Todo = () => {
2129
deleteGoal: handleDeleteTodo,
2230
} = useGoals();
2331

32+
const goals = useMemo(
33+
() =>
34+
myGoals.map((g) => ({
35+
id: String(g.id),
36+
title: g.title,
37+
completedTaskCount: g.completedTaskCount,
38+
totalTaskCount: g.totalTaskCount,
39+
achievementRate: g.achievementRate,
40+
})),
41+
[myGoals]
42+
);
43+
2444
const dateStr = formatDateToYYYYMMDD(selectedDate);
2545
const { myTasks, refetch: refetchTasks } = useTask({
2646
startDate: dateStr,
2747
endDate: dateStr,
2848
});
2949

30-
const todos = useMemo(() => {
31-
return goals.map((goal) => {
32-
const goalTasks = myTasks.filter((t) => t.goalId === goal.id);
33-
const completedCount = goalTasks.filter((t) => t.status === 'DONE').length;
50+
// 해당 날짜에 task가 있는 goal만 표시. task가 없으면 전체 goal 표시(할 일 추가 가능)
51+
const { todos, hiddenGoalIds } = useMemo(() => {
52+
const goalsWithTasksOnDate = goals.filter((goal) =>
53+
myTasks.some((t) => String(t.goalId) === goal.id)
54+
);
55+
const goalsToShow =
56+
goalsWithTasksOnDate.length > 0 ? goalsWithTasksOnDate : goals;
57+
const hiddenGoalIds = goals
58+
.filter((g) => !goalsToShow.some((s) => s.id === g.id))
59+
.map((g) => g.id);
60+
61+
const todosResult = goalsToShow.map((goal) => {
62+
const goalTasks = myTasks.filter(
63+
(t) => String(t.goalId) === goal.id
64+
);
65+
const completedCount = goalTasks.filter(
66+
(t) => t.status === 'DONE'
67+
).length;
3468
return {
3569
id: goal.id,
3670
title: goal.title,
@@ -43,8 +77,15 @@ export const Todo = () => {
4377
})),
4478
};
4579
});
80+
81+
return { todos: todosResult, hiddenGoalIds };
4682
}, [goals, myTasks]);
4783

84+
const hiddenGoals = useMemo(
85+
() => goals.filter((g) => hiddenGoalIds.includes(g.id)),
86+
[goals, hiddenGoalIds]
87+
);
88+
4889
const handlePrevDate = () => {
4990
setSelectedDate((prev) => {
5091
const next = new Date(prev);
@@ -62,26 +103,35 @@ export const Todo = () => {
62103
};
63104

64105
const [taskInputs, setTaskInputs] = useState<Record<string, string>>({});
106+
const [isAddOtherGoalsOpen, setIsAddOtherGoalsOpen] = useState(false);
107+
const isSubmittingRef = useRef(false);
65108
const [editingTask, setEditingTask] = useState<{
66109
goalId: string;
67110
taskId: string;
68111
} | null>(null);
69112
const [editingTaskInput, setEditingTaskInput] = useState('');
70113

71114
const handleAddTaskWithReset = async (goalId: string, title: string) => {
72-
await handleAddTask(goalId, title, dateStr);
73-
refetchTasks();
74-
setTaskInputs((prev) => ({ ...prev, [goalId]: '' }));
115+
const trimmed = title.trim();
116+
if (!trimmed) return;
117+
if (isSubmittingRef.current) return;
118+
119+
isSubmittingRef.current = true;
120+
try {
121+
await handleAddTask(goalId, trimmed, dateStr);
122+
refetchTasks();
123+
refetchGoals();
124+
setTaskInputs((prev) => ({ ...prev, [goalId]: '' }));
125+
} finally {
126+
isSubmittingRef.current = false;
127+
}
75128
};
76129

77130
const handleToggleTaskWrapper = (goalId: string, taskId: string) => {
78131
handleToggleTask(goalId, taskId);
79132
};
80133

81-
const handleDeleteTaskWrapper = async (
82-
goalId: string,
83-
taskId: string
84-
) => {
134+
const handleDeleteTaskWrapper = async (goalId: string, taskId: string) => {
85135
await handleDeleteTask(goalId, taskId);
86136
refetchTasks();
87137
};
@@ -92,6 +142,7 @@ export const Todo = () => {
92142
) => {
93143
if (e.key !== 'Enter') return;
94144
e.preventDefault();
145+
e.stopPropagation();
95146
const value = taskInputs[goalId] ?? '';
96147
handleAddTaskWithReset(goalId, value);
97148
};
@@ -136,7 +187,24 @@ export const Todo = () => {
136187
}
137188
};
138189

139-
if (todos.length === 0) {
190+
if (goalsLoading) {
191+
return (
192+
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 p-4">
193+
<div className="w-full max-w-2xl pt-8">
194+
<TodoHeader
195+
selectedDate={selectedDate}
196+
onPrevDate={handlePrevDate}
197+
onNextDate={handleNextDate}
198+
/>
199+
</div>
200+
<div className="flex flex-1 flex-col items-center justify-center">
201+
<p className="text-sub2">목표를 불러오는 중...</p>
202+
</div>
203+
</div>
204+
);
205+
}
206+
207+
if (goals.length === 0) {
140208
return (
141209
<div className="flex min-h-screen flex-col items-center bg-gray-50 p-4">
142210
<div className="w-full max-w-2xl pt-8">
@@ -172,7 +240,10 @@ export const Todo = () => {
172240
<AddGoalModal
173241
open={isAddGoalOpen}
174242
onOpenChange={setIsAddGoalOpen}
175-
onAddGoal={handleAddTodo}
243+
onAddGoal={async (title) => {
244+
await handleAddTodo(title);
245+
refetchGoals();
246+
}}
176247
/>
177248
</div>
178249
);
@@ -207,15 +278,58 @@ export const Todo = () => {
207278
onEditInputKeyDown={(taskId, e) =>
208279
handleEditInputKeyDown(todo.id, taskId, e)
209280
}
210-
onToggleTask={(taskId) =>
211-
handleToggleTaskWrapper(todo.id, taskId)
212-
}
213-
onDeleteTask={(taskId) =>
214-
handleDeleteTaskWrapper(todo.id, taskId)
215-
}
281+
onToggleTask={(taskId) => handleToggleTaskWrapper(todo.id, taskId)}
282+
onDeleteTask={(taskId) => handleDeleteTaskWrapper(todo.id, taskId)}
216283
onDeleteTodo={() => handleDeleteTodoWrapper(todo.id)}
217284
/>
218285
))}
286+
{hiddenGoals.length > 0 && (
287+
<div className="rounded-lg bg-white p-4 shadow-sm ring-1 ring-gray-200">
288+
<button
289+
type="button"
290+
onClick={() => setIsAddOtherGoalsOpen((prev) => !prev)}
291+
className="flex w-full items-center justify-between text-left text-base text-sub2 hover:text-main2"
292+
aria-expanded={isAddOtherGoalsOpen}
293+
aria-label={
294+
isAddOtherGoalsOpen
295+
? '다른 목표에 할 일 추가 접기'
296+
: '다른 목표에 할 일 추가 펼치기'
297+
}
298+
>
299+
<span>다른 목표에 할 일 추가</span>
300+
<span className="text-xl" aria-hidden>
301+
{isAddOtherGoalsOpen ? '−' : '+'}
302+
</span>
303+
</button>
304+
{isAddOtherGoalsOpen && (
305+
<div className="mt-4 space-y-3">
306+
{hiddenGoals.map((goal) => (
307+
<div
308+
key={goal.id}
309+
className="flex items-center gap-2 rounded-lg border border-gray-200 px-3 py-2"
310+
>
311+
<span className="shrink-0 text-sm text-sub2">
312+
{goal.title}
313+
</span>
314+
<Input
315+
type="text"
316+
placeholder="할 일 입력"
317+
value={taskInputs[goal.id] ?? ''}
318+
onChange={(e) =>
319+
handleTaskInputChange(goal.id, e.target.value)
320+
}
321+
onKeyDown={(e) =>
322+
handleTaskInputKeyDown(goal.id, e)
323+
}
324+
className="flex-1 border-none text-base focus-visible:ring-0"
325+
aria-label={`${goal.title}에 할 일 추가`}
326+
/>
327+
</div>
328+
))}
329+
</div>
330+
)}
331+
</div>
332+
)}
219333
<div className="flex items-center justify-center">
220334
<p className="mt-5 text-lg text-sub2">
221335
다른 목표를 만들고 싶다면 목표에서 추가할 수 있어요 →

0 commit comments

Comments
 (0)