1- import { useMemo , useState } from 'react' ;
1+ import { useMemo , useRef , useState } from 'react' ;
22import { Send } from 'lucide-react' ;
33import { Button } from 'shared/ui/components/button' ;
4+ import { Input } from 'shared/ui/components/input' ;
45import { useGoals } from 'shared/context/GoalsContext' ;
6+ import { useMyGoals } from 'features/goals/model/useMyGoals' ;
57import { useTask } from 'features/task/model/useTask' ;
68import { formatDateToYYYYMMDD } from 'shared/utils' ;
79import { AddGoalModal } from 'features/goals/ui/AddGoalModal' ;
@@ -11,8 +13,14 @@ import { TodoItem } from 'features/todo/TodoItem';
1113export 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