diff --git a/src/pages/AllowedServicePage/AllowedServicePage.tsx b/src/pages/AllowedServicePage/AllowedServicePage.tsx index fbb0b8c7..352aaf8d 100644 --- a/src/pages/AllowedServicePage/AllowedServicePage.tsx +++ b/src/pages/AllowedServicePage/AllowedServicePage.tsx @@ -5,9 +5,12 @@ import { useQueryClient } from '@tanstack/react-query'; import AutoFixedGrid from '@/shared/components/AutoFixedGrid/AutoFixedGrid'; import ModalContentsFriends from '@/shared/components/ModalContentsFriends/ModalContentsFriends'; import ModalWrapper, { ModalWrapperRef } from '@/shared/components/ModalWrapper/ModalWrapper'; +import NotificationPanel from '@/shared/components/NotificationPanel/NotificationPanel'; import Spacer from '@/shared/components/Spacer/Spacer'; import TextField from '@/shared/components/TextField/TextField'; +import useClickOutside from '@/shared/hooks/useClickOutside'; + import { isUrlValid } from '@/shared/utils/validation'; import { ColorPaletteType } from '@/shared/types/allowedService'; @@ -43,11 +46,16 @@ const AllowedServicePage = () => { const [isEditingTitle, setIsEditingTitle] = useState(false); const [urlInput, setUrlInput] = useState(''); const [selectedColor, setSelectedColor] = useState('#868C93'); + const [isNotificationVisible, setIsNotificationVisible] = useState(false); const queryClient = useQueryClient(); const friendsModalRef = useRef(null); const requireTitleModalRef = useRef(null); + + const bellIconRef = useRef(null); + const notificationPanelRef = useRef(null); + const handleChangeTitleInput = (e: ChangeEvent) => { setTitleInput(e.target.value); @@ -211,6 +219,24 @@ const AllowedServicePage = () => { } }; + const toggleNotification = () => { + setIsNotificationVisible((prev) => !prev); + }; + + useClickOutside( + notificationPanelRef, + (event) => { + if (!isNotificationVisible) return; + + if (bellIconRef.current && event && bellIconRef.current.contains(event.target as Node)) { + return; + } + + setIsNotificationVisible(false); + }, + isNotificationVisible, + ); + // NOTE: 첫 렌더링 시 api를 통해 받은 첫번째 allowed service group id를 activeGroupId로 설정 // 리스트 삭제 후, 현재 active 그룹이 리스트에 없는 경우 첫 번째 그룹으로 설정 useEffect(() => { @@ -252,7 +278,7 @@ const AllowedServicePage = () => { - @@ -356,9 +382,13 @@ const AllowedServicePage = () => { {({ isModalOpen }) => } + {() => } - + + + {isNotificationVisible && } + ); }; diff --git a/src/pages/HomePage/BoxCategory/BoxCategory.tsx b/src/pages/HomePage/BoxCategory/BoxCategory.tsx index 8330f4a5..ac7795e2 100644 --- a/src/pages/HomePage/BoxCategory/BoxCategory.tsx +++ b/src/pages/HomePage/BoxCategory/BoxCategory.tsx @@ -244,7 +244,10 @@ const BoxCategory = ({ onKeyDown={handleKeyDown} /> ) : ( -

+

{title}

)} @@ -257,12 +260,12 @@ const BoxCategory = ({ - + diff --git a/src/pages/HomePage/BoxTodayTodo/BoxTodayTodo.tsx b/src/pages/HomePage/BoxTodayTodo/BoxTodayTodo.tsx index e2017c2d..e0af9917 100644 --- a/src/pages/HomePage/BoxTodayTodo/BoxTodayTodo.tsx +++ b/src/pages/HomePage/BoxTodayTodo/BoxTodayTodo.tsx @@ -55,6 +55,7 @@ const BoxTodayTodo = ({ cancelComplete={cancelComplete} addingComplete={addingComplete} onCreateTodayTodos={onCreateTodayTodos} + addingTodayTodoStatus={addingTodayTodoStatus} /> ) : ( diff --git a/src/pages/HomePage/BoxTodayTodo/StatusAddBoxTodayTodo/StatusAddBoxTodayTodo.tsx b/src/pages/HomePage/BoxTodayTodo/StatusAddBoxTodayTodo/StatusAddBoxTodayTodo.tsx index a0d6849f..c704f646 100644 --- a/src/pages/HomePage/BoxTodayTodo/StatusAddBoxTodayTodo/StatusAddBoxTodayTodo.tsx +++ b/src/pages/HomePage/BoxTodayTodo/StatusAddBoxTodayTodo/StatusAddBoxTodayTodo.tsx @@ -24,6 +24,7 @@ interface StatusAddBoxTodayTodoProps { cancelComplete: () => void; addingComplete: boolean; onCreateTodayTodos: () => void; + addingTodayTodoStatus: boolean; } const StatusAddBoxTodayTodo = ({ @@ -35,6 +36,7 @@ const StatusAddBoxTodayTodo = ({ cancelComplete, addingComplete, onCreateTodayTodos, + addingTodayTodoStatus, }: StatusAddBoxTodayTodoProps) => { const { data: allowedServiceList } = useGetPopoverAllowedServiceList(); const registerServiceModalRef = useRef(null); @@ -82,6 +84,7 @@ const StatusAddBoxTodayTodo = ({ selectedNumber={selectedNumber} updateTodayTodos={deleteTodayTodos} addingComplete={addingComplete} + addingTodayTodoStatus={addingTodayTodoStatus} /> ); diff --git a/src/pages/HomePage/BoxTodayTodo/StatusDefaultBoxTodayTodo/StatusDefaultBoxTodayTodo.tsx b/src/pages/HomePage/BoxTodayTodo/StatusDefaultBoxTodayTodo/StatusDefaultBoxTodayTodo.tsx index fffefa47..90db4157 100644 --- a/src/pages/HomePage/BoxTodayTodo/StatusDefaultBoxTodayTodo/StatusDefaultBoxTodayTodo.tsx +++ b/src/pages/HomePage/BoxTodayTodo/StatusDefaultBoxTodayTodo/StatusDefaultBoxTodayTodo.tsx @@ -12,8 +12,8 @@ const StatusDefaultBoxTodayTodo = ({ hasTodos, onEnableAddStatus }: StatusDefaul

아직 오늘 할 일이 없어요

-

할 일을 추가하려면

-

+ 아이콘을 선택해주세요.

+

할 일을 추가해

+

타이머를 시작해 보세요.

diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index ecd55add..9181d23c 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -405,8 +405,13 @@ const HomePage = () => { )} -
)} @@ -425,8 +430,13 @@ const HomePage = () => { {dailyCategoryTask.length > 2 && (
-
)} diff --git a/src/pages/TimerPage/Carousel/CarouselFriend.tsx b/src/pages/TimerPage/Carousel/CarouselFriend.tsx index aea25cac..e0b6fbf9 100644 --- a/src/pages/TimerPage/Carousel/CarouselFriend.tsx +++ b/src/pages/TimerPage/Carousel/CarouselFriend.tsx @@ -43,7 +43,7 @@ const CarouselFriend = memo(function CarouselFriend({ return (
{/* 프로필 이미지 영역 */} - + {`${name}의 - {formattedTime} + + {formattedTime} +
{/* 이름 및 카테고리 표시 */} diff --git a/src/pages/TimerPage/MainTimer/MainTimer.tsx b/src/pages/TimerPage/MainTimer/MainTimer.tsx index 042f29ce..a7970b1e 100644 --- a/src/pages/TimerPage/MainTimer/MainTimer.tsx +++ b/src/pages/TimerPage/MainTimer/MainTimer.tsx @@ -1,6 +1,9 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; + +import { splitTasksByCompletion } from '@/shared/utils/timer'; import { getFormattedTimeInfo } from '@/pages/TimerPage/utils/timeFormat'; +import { useGetTimerTodos } from '@/shared/apisV2/timer/timer.queries'; import { useTimerContext } from '../contexts/TimerContext'; import TimerDisplay from './TimerDisplay/TimerDisplay'; @@ -13,18 +16,34 @@ import TimerHeader from './TimerHeader/TimerHeader'; */ const MainTimer = () => { // 타이머 컨텍스트에서 필요한 상태와 액션만 가져오기 - const { timer, elapsedTime, totalElapsedTimeOfToday, isPlaying, selectedTask, actions } = useTimerContext(); + const { todayFormattedDate, timer, elapsedTime, totalElapsedTimeOfToday, isPlaying, selectedTask, actions } = + useTimerContext(); + + // 할일 데이터 조회 + const { data: todosData } = useGetTimerTodos({ targetDate: todayFormattedDate }); + + // 완료된 할 일 목록 가져오기 + const { completedTodos } = useMemo(() => { + const todos = todosData?.data?.task ?? []; + return splitTasksByCompletion(todos); + }, [todosData]); // 작업이 선택되어 있는지 여부 const hasSelectedTask = selectedTask.id !== null; + // 선택된 작업이 완료되었는지 확인 + const isSelectedTaskCompleted = useCallback(() => { + if (!selectedTask.id) return false; + return completedTodos.some((todo) => todo.id === selectedTask.id); + }, [completedTodos, selectedTask.id]); + // 재생/정지 토글 핸들러 - 작업이 선택되지 않은 경우 처리 방지 const handlePlayPauseToggle = useCallback(() => { - if (!hasSelectedTask) return; + if (!hasSelectedTask || isSelectedTaskCompleted()) return; // 현재 상태의 반대로 토글 actions.togglePlay(!isPlaying); - }, [isPlaying, hasSelectedTask, actions]); + }, [isPlaying, hasSelectedTask, isSelectedTaskCompleted, actions]); // 표시할 시간 정보 계산 - 유틸 함수로 분리하여 관심사 분리 const timeInfo = getFormattedTimeInfo(timer, elapsedTime, totalElapsedTimeOfToday); @@ -36,6 +55,7 @@ const MainTimer = () => { selectedTaskName={selectedTask.name} selectedTaskCategoryName={selectedTask.categoryName} hasSelectedTask={hasSelectedTask} + isCompleted={isSelectedTaskCompleted()} /> {/* 타이머 디스플레이 - 시간 표시 및 제어 버튼 */} @@ -45,6 +65,7 @@ const MainTimer = () => { timer={timer} isPlaying={isPlaying} onToggle={handlePlayPauseToggle} + disabled={!hasSelectedTask || isSelectedTaskCompleted()} />
); diff --git a/src/pages/TimerPage/MainTimer/TimerDisplay/ButtonTimerPlay/ButtonTimerPlay.tsx b/src/pages/TimerPage/MainTimer/TimerDisplay/ButtonTimerPlay/ButtonTimerPlay.tsx index d7241329..94fc857f 100644 --- a/src/pages/TimerPage/MainTimer/TimerDisplay/ButtonTimerPlay/ButtonTimerPlay.tsx +++ b/src/pages/TimerPage/MainTimer/TimerDisplay/ButtonTimerPlay/ButtonTimerPlay.tsx @@ -1,3 +1,5 @@ +import { MouseEvent } from 'react'; + import PauseIcon from '@/shared/assets/svgs/defaultpause.svg?react'; import PlayIcon from '@/shared/assets/svgs/defaultplay.svg?react'; import HoverPauseIcon from '@/shared/assets/svgs/hoverpause.svg?react'; @@ -6,14 +8,15 @@ import HoverPlayIcon from '@/shared/assets/svgs/hoverplay.svg?react'; interface ButtonTimerPlayProps { onClick: () => void; isPlaying: boolean; + disabled?: boolean; } -const ButtonTimerPlay = ({ onClick, isPlaying }: ButtonTimerPlayProps) => { +const ButtonTimerPlay = ({ onClick, isPlaying, disabled = false }: ButtonTimerPlayProps) => { const IconComponent = isPlaying ? PauseIcon : PlayIcon; const HoverIconComponent = isPlaying ? HoverPauseIcon : HoverPlayIcon; return ( - diff --git a/src/pages/TimerPage/MainTimer/TimerDisplay/TimerDisplay.tsx b/src/pages/TimerPage/MainTimer/TimerDisplay/TimerDisplay.tsx index 7909fe97..754a9453 100644 --- a/src/pages/TimerPage/MainTimer/TimerDisplay/TimerDisplay.tsx +++ b/src/pages/TimerPage/MainTimer/TimerDisplay/TimerDisplay.tsx @@ -14,6 +14,7 @@ interface TimerDisplayProps { timer: number; isPlaying: boolean; onToggle: () => void; + disabled?: boolean; } /** @@ -21,7 +22,14 @@ interface TimerDisplayProps { * * 타이머의 시간 표시, 상태 텍스트 및 재생/정지 버튼을 포함하는 UI */ -const TimerDisplay = ({ statusText, formattedTimeText, timer, isPlaying, onToggle }: TimerDisplayProps) => { +const TimerDisplay = ({ + statusText, + formattedTimeText, + timer, + isPlaying, + onToggle, + disabled = false, +}: TimerDisplayProps) => { return (
@@ -32,7 +40,7 @@ const TimerDisplay = ({ statusText, formattedTimeText, timer, isPlaying, onToggl {formattedTimeText}
- + ); diff --git a/src/pages/TimerPage/MainTimer/TimerHeader/TimerHeader.tsx b/src/pages/TimerPage/MainTimer/TimerHeader/TimerHeader.tsx index 271e5b4b..0004b209 100644 --- a/src/pages/TimerPage/MainTimer/TimerHeader/TimerHeader.tsx +++ b/src/pages/TimerPage/MainTimer/TimerHeader/TimerHeader.tsx @@ -7,6 +7,7 @@ interface TimerHeaderProps { selectedTaskName: string; selectedTaskCategoryName: string; hasSelectedTask: boolean; + isCompleted?: boolean; } /** @@ -14,7 +15,12 @@ interface TimerHeaderProps { * * 선택된 할일의 이름과 카테고리를 표시하거나, 할일이 선택되지 않은 경우 안내 메시지를 표시. */ -const TimerHeader = ({ selectedTaskName, selectedTaskCategoryName, hasSelectedTask }: TimerHeaderProps) => { +const TimerHeader = ({ + selectedTaskName, + selectedTaskCategoryName, + hasSelectedTask, + isCompleted = false, +}: TimerHeaderProps) => { if (!hasSelectedTask) { return (
diff --git a/src/pages/TimerPage/SideMenuTimer/SideMenuTimer.tsx b/src/pages/TimerPage/SideMenuTimer/SideMenuTimer.tsx index 2e8561eb..4c24df10 100644 --- a/src/pages/TimerPage/SideMenuTimer/SideMenuTimer.tsx +++ b/src/pages/TimerPage/SideMenuTimer/SideMenuTimer.tsx @@ -84,6 +84,16 @@ const SideMenuTimer = ({ ongoingTodos = [], completedTodos = [] }: SideMenuTimer setCompletedTodoToggle(true); } + // 선택된 할 일의 상태가 변경되면 타이머 정지 및 남은 할 일 중 첫번 째 할 일 선택 + if (selectedTask.id === taskId && actions.stopCurrentTimer) { + actions.stopCurrentTimer(selectedTask.id); + const remainingTodos = ongoingTodos.filter((todo) => todo.id !== taskId); + if (remainingTodos.length > 0) { + const next = remainingTodos[0]; + actions.selectTask(next.id, next.elapsedTime, next.name, next.categoryName); + } + } + queryClient.invalidateQueries({ queryKey: timerKeys.todos({ targetDate: todayFormattedDate }), }); @@ -91,7 +101,7 @@ const SideMenuTimer = ({ ongoingTodos = [], completedTodos = [] }: SideMenuTimer }, ); }, - [toggleTaskStatus, todayFormattedDate, queryClient], + [toggleTaskStatus, todayFormattedDate, queryClient, selectedTask.id, actions, ongoingTodos], ); // 할일 항목 렌더링 함수 - 최적화됨 @@ -102,9 +112,13 @@ const SideMenuTimer = ({ ongoingTodos = [], completedTodos = [] }: SideMenuTimer {...todo} isSelected={todo.id === selectedTask.id} onClick={() => handleTodoClick(todo)} - onToggleComplete={() => handleToggleTodoComplete(todo.id, isOngoing)} + onToggleComplete={(e) => { + e.stopPropagation(); + handleToggleTodoComplete(todo.id, isOngoing); + }} timerIncreasedTime={getTimerIncreasedTime(todo.id, todo.elapsedTime, selectedTask.id, timer)} undeletable={true} + disableHoverCalendar={true} /> ), [handleTodoClick, handleToggleTodoComplete, selectedTask.id, timer], diff --git a/src/shared/components/BoxTodo/BoxTodo.tsx b/src/shared/components/BoxTodo/BoxTodo.tsx index 627c9195..6adf8416 100644 --- a/src/shared/components/BoxTodo/BoxTodo.tsx +++ b/src/shared/components/BoxTodo/BoxTodo.tsx @@ -29,7 +29,7 @@ interface BoxTodoProps { isSelected?: boolean; selectedNumber?: number; onClick?: () => void; - onToggleComplete?: () => void; + onToggleComplete?: (e: MouseEvent) => void; updateTodayTodos?: (todo: Omit) => void; clickable?: boolean; addingComplete?: boolean; @@ -38,6 +38,8 @@ interface BoxTodoProps { onPatchTask?: (taskId: number, name: string, startDate: string, endDate: string | null) => void; activeCalendarTask?: boolean; undeletable?: boolean; + disableHoverCalendar?: boolean; + addingTodayTodoStatus?: boolean; } const BoxTodo = ({ @@ -59,6 +61,7 @@ const BoxTodo = ({ onPatchTask, activeCalendarTask, undeletable = false, + disableHoverCalendar = false, }: BoxTodoProps) => { const { mutate: deleteTask } = useDeleteTask(); @@ -134,7 +137,13 @@ const BoxTodo = ({
- {isEditing ? ( @@ -148,7 +157,9 @@ const BoxTodo = ({ /> ) : (

{name} @@ -175,7 +186,9 @@ const BoxTodo = ({

-

+

{friend.isOnline ? '온라인' : '오프라인'}

-

+

{formatSecondsForFriendsList(friend.elapsedTime)}

-
+
+
deleteFriend({ friendId: friend.id })} label="친구삭제" textColor="red" /> diff --git a/src/shared/components/NotificationPanel/NotificationPanel.tsx b/src/shared/components/NotificationPanel/NotificationPanel.tsx index 5531ca65..8539d498 100644 --- a/src/shared/components/NotificationPanel/NotificationPanel.tsx +++ b/src/shared/components/NotificationPanel/NotificationPanel.tsx @@ -6,7 +6,7 @@ const NotificationPanel = forwardRef((_, ref) => { ref={ref} className="absolute right-[3.2rem] top-[11.5rem] h-[38.2rem] w-[36.9rem] rounded-[14px] bg-gray-bg-03 p-[2.8rem] drop-shadow-calendarDrop" > -

알림

+

알림

아직 받은 알림이 없어요.

diff --git a/src/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx b/src/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx index b4b9996a..970db44b 100644 --- a/src/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx +++ b/src/shared/layout/Sidebar/ModalContentsSetting/AccountContent/AccountContent.tsx @@ -79,7 +79,7 @@ const AccountContent = ({ ...props }: AccountContentProps) => {

데스크톱 푸시 알림

-

데스크톱 앱에서 작성의 푸시 알림을 즉시 받으세요.

+

데스크톱 앱에서 모립의 푸시 알림을 즉시 받으세요.

@@ -100,7 +100,9 @@ const AccountContent = ({ ...props }: AccountContentProps) => {

내 계정 삭제

-

본 기기를 포함한 모든 기기에서 로그아웃합니다.

+

+ 계정을 영구적으로 삭제하고 모든 워크스페이스에서 액세스 권한을 제거합니다. +