Repository: TEAM-DAWM/SHELLIN-CLIENT
Pull request #439: [REFACTOR] task click logic update
Author: login: Kjiw0n
Labels: name: 🍀 지원
name: v2
name: 💪 기능 개선
Created: 2025-04-06T12:05:48Z | Merged: 2025-04-09T10:31:50Z

<!-- PR 제목은 관련 이슈번호의 제목과 동일한 제목!! -->

## 작업 내용 :technologist:

- 선택된 task를 zustand를 도입해 관리했습니다. (`useTaskSelectionStore`)
  - `selectedTarget` -> `selectedTask`로 상태명 명확하게 변경
  - `setSelectedTask`, `clearSelectedTask`를 통해 선택된 task 관리
  - isDragging, setIsDragging을 통해 캘린더에서 드래그 통해 timeblock 생성 시 로직 관리
- 클릭 상태 가지고있는 task에 css 적용 (clicked 일때도 pressed와 똑같이 적용)
- 캘린더 외에 외부영역 클릭 시 클릭 상태 해제 ~~(`useOutsideTaskClick`)~~ (Today 컴포넌트의 `handleClickOutside`)
- timeblock 하나 생성 시 클릭 상태 해제 (`clearSelectedTask`)
- sub domain 배포 위한 브랜치명 변경 (develop -> main), 커밋 간결화 및 .github/ 코드 전달x(액션 방지)


## 알게된 점 :rocket:

> 기록하며 개발하기!

- 

## 리뷰 요구사항 :speech_balloon:

> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

- 외부영역 클릭 시 `clearSelectedTask()`을 호출하려면(: 클릭 잃게 하려면) 풀캘린더의 외부아이템 드롭을 통한 이벤트생성을 못하고있는데, 좋은 방법 떠오르시면 답글 부탁드립니다!

## 관련 이슈

close #435 

## 스크린샷 (선택)


Linked issue #435: [REFACTOR] timeblock 할당 로직 개선
## 어떤 기능인가요?

> 추가하려는 기능에 대해 간결하게 설명해주세요

task 클릭 후 timeblock 생성하는 기능 중, 클릭 상태를 특정 상황 이외엔 해제하도록 처리

## 작업 상세 내용

- [x] 캘린더 외에 외부영역 클릭 시 클릭 상태 해제
- [x] timeblock 하나 생성 시 클릭 상태 해제
- [x] 클릭 상태 가지고있는 task에 css 적용 (clicked 일때도 pressed와 똑같이 적용)

## 참고할만한 자료(선택)


Diff:
diff --git a/.github/workflows/test-link-deploy.yml b/.github/workflows/test-link-deploy.yml
index 9af73d0b..bf3f820e 100644
--- a/.github/workflows/test-link-deploy.yml
+++ b/.github/workflows/test-link-deploy.yml
@@ -22,5 +22,9 @@ jobs:
         run: |
           git config user.name "Kjiw0n"
           git config user.email "rwd0904@naver.com"
+          git reset $(git rev-list --max-parents=0 HEAD)
+          git add .
+          git reset -- .github/
+          git commit -m "chore: deploy on $(date +'%Y-%m-%d %H:%M')"
           git remote add test "$TEST_DEPLOY_REPO"
           git push --force test develop
diff --git a/package.json b/package.json
index c5ab0f48..bbd75ecf 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,8 @@
     "react-datepicker": "^7.3.0",
     "react-dom": "^18.3.1",
     "react-router-dom": "^6.24.1",
-    "vite-plugin-svgr": "^4.3.0"
+    "vite-plugin-svgr": "^4.3.0",
+    "zustand": "^5.0.3"
   },
   "devDependencies": {
     "@chromatic-com/storybook": "^3.2.2",
diff --git a/src/components/common/StagingArea/StagingArea.tsx b/src/components/common/StagingArea/StagingArea.tsx
index 51b4ca1b..327ab8b6 100644
--- a/src/components/common/StagingArea/StagingArea.tsx
+++ b/src/components/common/StagingArea/StagingArea.tsx
@@ -9,7 +9,7 @@ import FilterSection from '@/components/targetArea/FilterSection';
 import { StagingAreaProps } from '@/types/area/taskAreaType';
 
 function StagingArea(props: StagingAreaProps) {
-	const { handleSelectedTarget, selectedTarget, tasks, sortOrder, handleSortOrder, targetDate, isStagingOpen } = props;
+	const { tasks, sortOrder, handleSortOrder, targetDate, isStagingOpen } = props;
 
 	return (
 		<StagingAreaLayout isOpen={isStagingOpen}>
@@ -25,12 +25,7 @@ function StagingArea(props: StagingAreaProps) {
 				<Droppable droppableId="staging">
 					{(provided) => (
 						<SizedWrapper ref={provided.innerRef} {...provided.droppableProps} style={{ height: '100%' }}>
-							<StagingAreaTaskContainer
-								handleSelectedTarget={handleSelectedTarget}
-								selectedTarget={selectedTarget}
-								tasks={tasks}
-								targetDate={targetDate}
-							/>
+							<StagingAreaTaskContainer tasks={tasks} targetDate={targetDate} />
 							{provided.placeholder}
 						</SizedWrapper>
 					)}
diff --git a/src/components/common/StagingArea/StagingAreaTaskContainer.tsx b/src/components/common/StagingArea/StagingAreaTaskContainer.tsx
index e5f4b01a..b99411af 100644
--- a/src/components/common/StagingArea/StagingAreaTaskContainer.tsx
+++ b/src/components/common/StagingArea/StagingAreaTaskContainer.tsx
@@ -7,17 +7,18 @@ import BtnTaskContainer from '../BtnTaskContainer';
 import Todo from '../v2/taskBox/Todo';
 
 import EmptyViewStaging from '@/components/common/EmptyViewStaging';
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
 import { StatusType, TaskType } from '@/types/tasks/taskType';
 import { formatDatetoStringKor } from '@/utils/formatDateTime';
 
 interface StagingAreaTaskContainerProps {
-	handleSelectedTarget: (task: TaskType | null) => void;
-	selectedTarget: TaskType | null;
 	tasks: TaskType[];
 	targetDate?: string;
 }
 
-function StagingAreaTaskContainer({ handleSelectedTarget, selectedTarget, tasks }: StagingAreaTaskContainerProps) {
+function StagingAreaTaskContainer({ tasks }: StagingAreaTaskContainerProps) {
+	const { selectedTask } = useTaskSelectionStore();
+
 	useEffect(() => {
 		const container = document.getElementById('dumping-task-container');
 
@@ -25,7 +26,7 @@ function StagingAreaTaskContainer({ handleSelectedTarget, selectedTarget, tasks
 			const draggable = new FullCalendarDraggable(container, {
 				itemSelector: '.todo-item', // 드래그 가능한 요소
 				eventData: () => {
-					if (selectedTarget) {
+					if (selectedTask) {
 						setTimeout(() => {
 							const mirrorElement = document.querySelector('.fc-event-dragging');
 							if (mirrorElement) {
@@ -34,8 +35,8 @@ function StagingAreaTaskContainer({ handleSelectedTarget, selectedTarget, tasks
 							}
 						}, 0);
 						return {
-							id: selectedTarget.id.toString(),
-							title: selectedTarget.name,
+							id: selectedTask.id.toString(),
+							title: selectedTask.name,
 						};
 					}
 					return null;
@@ -49,7 +50,7 @@ function StagingAreaTaskContainer({ handleSelectedTarget, selectedTarget, tasks
 		}
 
 		return undefined;
-	}, [selectedTarget]);
+	}, [selectedTask]);
 
 	return (
 		<StagingAreaTaskContainerLayout>
@@ -83,8 +84,8 @@ function StagingAreaTaskContainer({ handleSelectedTarget, selectedTarget, tasks
 												deadlineTime={task.deadLine?.time || undefined}
 												isStatusVisible={false}
 												taskId={task.id}
+												task={task}
 												targetDate=""
-												onClick={() => handleSelectedTarget(task)}
 											/>
 										</div>
 									)}
diff --git a/src/components/common/fullCalendar/FullCalendarBox.tsx b/src/components/common/fullCalendar/FullCalendarBox.tsx
index 50c823f2..08bdd98a 100644
--- a/src/components/common/fullCalendar/FullCalendarBox.tsx
+++ b/src/components/common/fullCalendar/FullCalendarBox.tsx
@@ -24,17 +24,18 @@ import FullCalendarLayout from '@/components/common/fullCalendar/FullCalendarSty
 import customSlotLabelContent from '@/components/common/fullCalendar/fullCalendarUtils';
 import MODAL from '@/constants/modalLocation';
 import { STATUSES } from '@/constants/statuses';
-import { TaskType } from '@/types/tasks/taskType';
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
 import { formatDateToLocal, formatDatetoLocalDate } from '@/utils/formatDateTime';
 
 interface FullCalendarBoxProps {
 	size: 'small' | 'big';
 	selectDate?: Date | null;
-	selectedTarget?: TaskType | null;
 	handleChangeDate: (target: Date) => void;
 }
 
-function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }: FullCalendarBoxProps) {
+function FullCalendarBox({ size, selectDate, handleChangeDate }: FullCalendarBoxProps) {
+	const { selectedTask, clearSelectedTask, setIsDragging } = useTaskSelectionStore();
+
 	const today = useMemo(() => new Date(), []);
 	const todayDate = today.toISOString().split('T')[0];
 	const [currentView, setCurrentView] = useState('timeGridWeek');
@@ -73,7 +74,6 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 	const { mutateAsync: updateMutate } = useUpdateTimeBlock();
 	const { mutate: deleteMutate } = useDeleteTimeBlock();
 
-	// 추후 전역상태로 처리 예정
 	interface EventData {
 		title: string;
 		start: string;
@@ -104,7 +104,6 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 
 	const handleViewChange = (view: ViewMountArg) => {
 		setCurrentView(view.view.type);
-		// updateRange(view.view.type);
 	};
 
 	const handleDatesSet = (dateInfo: DatesSetArg) => {
@@ -148,6 +147,7 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 
 	const handleEventClick = (info: EventClickArg) => {
 		const now = Date.now();
+		info.jsEvent.stopPropagation();
 
 		// 0.3초 내에 클릭 시 더블클릭으로 간주
 		if (now - lastClickTime < 300) {
@@ -175,11 +175,6 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 		setMainModalOpen(false);
 		setSelectedTaskId(null);
 		setSelectedTimeBlockId(null);
-
-		/** TODO:
-		 * 닫힐 때 이벤트 생성하기
-		 * createMutate({ taskId: Number(info.event.id), startTime: start, endTime: end });
-		 * */
 	};
 
 	const removeTimezone = (str: string, isAlltime: boolean = false) => {
@@ -197,12 +192,12 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 	};
 
 	const addEventWhenDragged = async (selectInfo: DateSelectArg) => {
-		if (calendarRef.current && selectedTarget && selectedTarget.id !== -1) {
+		if (calendarRef.current && selectedTask && selectedTask.id !== -1) {
 			const calendarApi = calendarRef.current.getApi();
 
 			const existingEvents = calendarApi.getEvents();
 			existingEvents.forEach((event) => {
-				if (event.id === selectedTarget.id.toString()) {
+				if (event.id === selectedTask.id.toString()) {
 					event.remove();
 				}
 			});
@@ -223,14 +218,14 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 			}
 
 			calendarApi.addEvent({
-				id: selectedTarget.id.toString(),
-				title: selectedTarget.name,
+				id: selectedTask.id.toString(),
+				title: selectedTask.name,
 				start: selectInfo.startStr,
 				end: selectInfo.endStr,
 				allDay: selectInfo.allDay,
 				classNames: 'tasks',
 				extendedProps: {
-					taskId: selectedTarget.id,
+					taskId: selectedTask.id,
 					timeBlockId: null,
 				},
 			});
@@ -240,7 +235,7 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 
 			try {
 				await createMutate({
-					taskId: selectedTarget.id,
+					taskId: selectedTask.id,
 					startTime: startStr,
 					endTime: endStr,
 					isAllTime: selectInfo.allDay,
@@ -248,8 +243,9 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 			} catch (error) {
 				console.error('addEventWhenDragged error:', error);
 				// conflict 에러 발생 시 복구
-				calendarApi.getEventById(selectedTarget.id.toString())?.remove();
+				calendarApi.getEventById(selectedTask.id.toString())?.remove();
 			}
+			clearSelectedTask();
 		}
 	};
 
@@ -259,7 +255,7 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 			calendarApi.unselect();
 		}
 
-		if (selectedTarget && selectedTarget.id !== -1) {
+		if (selectedTask && selectedTask.id !== -1) {
 			addEventWhenDragged(selectInfo);
 		}
 	};
@@ -296,7 +292,7 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 		}
 	};
 
-	const isSelectable = !!selectedTarget;
+	const isSelectable = !!selectedTask;
 
 	const handleDelete = () => {
 		if (selectedTaskId && selectedTimeBlockId) {
@@ -378,6 +374,8 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 			console.error('handleEventReceive error:', error);
 			info.event.remove(); // 추가된 이벤트 제거
 		}
+
+		clearSelectedTask();
 	};
 
 	// CalendarSettingDropdown handler
@@ -489,6 +487,12 @@ function FullCalendarBox({ size, selectDate, selectedTarget, handleChangeDate }:
 				eventDrop={updateEvent} // 기존 이벤트 드래그 수정 핸들러
 				eventResize={updateEvent} // 기존 이벤트 리사이즈 수정 핸들러
 				eventReceive={(info) => handleEventReceive(info)}
+				eventDragStart={() => {
+					setIsDragging(true);
+				}}
+				eventDragStop={() => {
+					setIsDragging(false);
+				}}
 			/>
 			{isCalendarPopupOpen && (
 				<DateCorrectionModal
diff --git a/src/components/common/v2/taskBox/Todo.tsx b/src/components/common/v2/taskBox/Todo.tsx
index c988226d..dc006c1b 100644
--- a/src/components/common/v2/taskBox/Todo.tsx
+++ b/src/components/common/v2/taskBox/Todo.tsx
@@ -6,7 +6,8 @@ import DropdownButton from '../control/DropdownButton';
 
 import useUpdateTaskStatus from '@/apis/tasks/updateTaskStatus/query';
 import MainSettingModal from '@/components/common/v2/modal/MainSettingModal';
-import { STATUS } from '@/types/tasks/taskType';
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
+import { STATUS, TaskType } from '@/types/tasks/taskType';
 import { formatTimeToDueTime } from '@/utils/formatDateTime';
 
 type StatusType = (typeof STATUS)[keyof typeof STATUS];
@@ -17,9 +18,9 @@ type TodoProps = {
 	deadlineTime?: string;
 	status: StatusType;
 	isStatusVisible?: boolean;
-	onClick: () => void;
 	preventDoubleClick?: boolean;
 	taskId: number;
+	task: TaskType;
 	targetDate: string;
 };
 
@@ -29,19 +30,30 @@ function Todo({
 	status: initStatus,
 	isStatusVisible = true,
 	deadlineTime,
-	onClick,
 	taskId,
+	task,
 	preventDoubleClick,
 	targetDate,
 }: TodoProps) {
 	const [status, setStatus] = useState<StatusType>(initStatus);
 	const isCompleted = status === STATUS.COMPLETE;
+	const { selectedTask, setSelectedTask, clearSelectedTask } = useTaskSelectionStore();
+	const isSelected = selectedTask?.id === taskId;
 
 	const [isModalOpen, setModalOpen] = useState(false);
 
 	useEffect(() => {
 		setStatus(initStatus);
 	}, [initStatus]);
+
+	const handleTaskClick = () => {
+		if (selectedTask && selectedTask.id === task.id) {
+			clearSelectedTask();
+		} else {
+			setSelectedTask(task);
+		}
+	};
+
 	/** 모달 띄우기 */
 	const handleDoubleClick = (e: React.MouseEvent) => {
 		if (preventDoubleClick) {
@@ -69,7 +81,13 @@ function Todo({
 	return (
 		<>
 			<div className="todo-item">
-				<TodoContainer isCompleted={isCompleted} onDoubleClick={handleDoubleClick} draggable onClick={onClick}>
+				<TodoContainer
+					isCompleted={isCompleted}
+					isSelected={isSelected}
+					onDoubleClick={handleDoubleClick}
+					draggable
+					onClick={() => handleTaskClick()}
+				>
 					<TodoWrapper>
 						<span className="todo-title">{title}</span>
 						{deadlineDate && (
@@ -137,7 +155,7 @@ const textStyles = ({ theme, isCompleted }: { theme: Theme; isCompleted: boolean
 	}
 `;
 
-const TodoContainer = styled.div<{ isCompleted: boolean }>`
+const TodoContainer = styled.div<{ isCompleted: boolean; isSelected: boolean }>`
 	${({ theme }) => baseStyles({ theme })}
 	${({ theme, isCompleted }) => textStyles({ theme, isCompleted })}
 	border: 1px solid
@@ -152,6 +170,13 @@ const TodoContainer = styled.div<{ isCompleted: boolean }>`
 		background-color: ${({ theme }) => theme.colorToken.Component.strong};
 		border: 1px solid ${({ theme }) => theme.colorToken.Outline.primaryStrong};
 	}
+
+	${({ isSelected, theme }) =>
+		isSelected &&
+		`
+        background-color: ${theme.colorToken.Component.strong};
+        border: 1px solid ${theme.colorToken.Outline.primaryStrong};
+    `}
 `;
 
 const TodoWrapper = styled.div`
diff --git a/src/components/targetArea/TargetArea.tsx b/src/components/targetArea/TargetArea.tsx
index 464b1778..fdb27d9a 100644
--- a/src/components/targetArea/TargetArea.tsx
+++ b/src/components/targetArea/TargetArea.tsx
@@ -11,8 +11,6 @@ import { formatDatetoLocalDate } from '@/utils/formatDateTime';
 
 function TargetArea(props: TargetAreaProps) {
 	const {
-		handleSelectedTarget,
-		selectedTarget,
 		tasks,
 		onClickPrevDate,
 		onClickNextDate,
@@ -47,12 +45,7 @@ function TargetArea(props: TargetAreaProps) {
 			<Droppable droppableId="target">
 				{(provided) => (
 					<DroppableWrapper ref={provided.innerRef} {...provided.droppableProps} style={{ height: '100%' }}>
-						<TargetTaskSection
-							handleSelectedTarget={handleSelectedTarget}
-							selectedTarget={selectedTarget}
-							tasks={tasks}
-							targetDate={formatDatetoLocalDate(targetDate)}
-						/>
+						<TargetTaskSection tasks={tasks} targetDate={formatDatetoLocalDate(targetDate)} />
 						{provided.placeholder}
 					</DroppableWrapper>
 				)}
diff --git a/src/components/targetArea/TargetTaskSection.tsx b/src/components/targetArea/TargetTaskSection.tsx
index 61b61ea5..26548018 100644
--- a/src/components/targetArea/TargetTaskSection.tsx
+++ b/src/components/targetArea/TargetTaskSection.tsx
@@ -7,16 +7,17 @@ import BtnTaskContainer from '../common/BtnTaskContainer';
 
 import EmptyViewToday from '@/components/common/EmptyViewToday';
 import Todo from '@/components/common/v2/taskBox/Todo';
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
 import { TaskType } from '@/types/tasks/taskType';
 import { formatDatetoStringKor } from '@/utils/formatDateTime';
 
 interface TargetTaskSectionProps {
-	handleSelectedTarget: (task: TaskType | null) => void;
-	selectedTarget: TaskType | null;
 	tasks: TaskType[];
 	targetDate: string;
 }
-function TargetTaskSection({ handleSelectedTarget, selectedTarget, tasks, targetDate }: TargetTaskSectionProps) {
+function TargetTaskSection({ tasks, targetDate }: TargetTaskSectionProps) {
+	const { selectedTask } = useTaskSelectionStore();
+
 	useEffect(() => {
 		const container = document.getElementById('todolist-task-container');
 		// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -25,7 +26,7 @@ function TargetTaskSection({ handleSelectedTarget, selectedTarget, tasks, target
 			const draggable = new FullCalendarDraggable(container, {
 				itemSelector: '.todo-item',
 				eventData: () => {
-					if (selectedTarget) {
+					if (selectedTask) {
 						setTimeout(() => {
 							const mirrorElement = document.querySelector('.fc-event-dragging');
 							if (mirrorElement) {
@@ -34,8 +35,8 @@ function TargetTaskSection({ handleSelectedTarget, selectedTarget, tasks, target
 							}
 						}, 0);
 						return {
-							id: selectedTarget.id.toString(),
-							title: selectedTarget.name,
+							id: selectedTask.id.toString(),
+							title: selectedTask.name,
 						};
 					}
 					return null;
@@ -49,7 +50,7 @@ function TargetTaskSection({ handleSelectedTarget, selectedTarget, tasks, target
 		}
 
 		return undefined;
-	}, [selectedTarget]);
+	}, [selectedTask]);
 
 	return (
 		<BtnTaskContainer id="todolist-task-container" type="target">
@@ -80,8 +81,8 @@ function TargetTaskSection({ handleSelectedTarget, selectedTarget, tasks, target
 										deadlineDate={formatDatetoStringKor(task.deadLine?.date)}
 										deadlineTime={task.deadLine?.time || undefined}
 										taskId={task.id}
+										task={task}
 										targetDate={targetDate}
-										onClick={() => handleSelectedTarget(task)}
 										status={task.status}
 									/>
 								</TodoSizedWrapper>
diff --git a/src/pages/Today.tsx b/src/pages/Today.tsx
index 37aa30b6..c88869a2 100644
--- a/src/pages/Today.tsx
+++ b/src/pages/Today.tsx
@@ -11,12 +11,11 @@ import NavBar from '@/components/common/NavBar';
 import StagingArea from '@/components/common/StagingArea/StagingArea';
 import TargetArea from '@/components/targetArea/TargetArea';
 import { SortOrderType } from '@/constants/sortType';
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
 import { TaskType } from '@/types/tasks/taskType';
 import { formatDatetoLocalDate } from '@/utils/formatDateTime';
 
 function Today() {
-	const [selectedTarget, setSelectedTarget] = useState<TaskType | null>(null);
-
 	const storedStagingSortOrder = localStorage.getItem('stagingSortOrder') as SortOrderType | null;
 	const storedTargetSortOrder = localStorage.getItem('targetSortOrder') as SortOrderType | null;
 
@@ -28,6 +27,8 @@ function Today() {
 	const targetDate = formatDatetoLocalDate(selectedDate);
 	const [isDumpAreaOpen, setDumpAreaOpen] = useState(true);
 
+	const { isDragging, clearSelectedTask } = useTaskSelectionStore();
+
 	// Task 목록 Get
 	const { data: stagingData } = useGetTasks({ sortOrder: stagingSortOrder });
 	const { data: targetData, isError: isTargetError } = useGetTasks({ sortOrder: targetSortOrder, targetDate });
@@ -47,10 +48,6 @@ function Today() {
 		}
 	};
 
-	const handleSelectedTarget = (task: TaskType | null) => {
-		setSelectedTarget(task);
-	};
-
 	const handlePrevBtn = () => {
 		const newDate = new Date(selectedDate);
 		newDate.setDate(newDate.getDate() - 1);
@@ -146,15 +143,23 @@ function Today() {
 				taskList: newOrder,
 			});
 		}
+		clearSelectedTask();
+	};
+
+	const handleClickOutside = (e: React.MouseEvent) => {
+		const target = e.target as HTMLElement;
+
+		if (!target.closest('.todo-item') && !target.closest('.fc-view-harness fc-view-harness-active') && !isDragging) {
+			// 클릭된 요소가 todo-item 또는 fc-view-harness(캘린더 이벤트 추가 영역) 내부가 아닐 경우
+			clearSelectedTask();
+		}
 	};
 
 	return (
-		<TodayLayout>
+		<TodayLayout onClick={handleClickOutside}>
 			<NavBar isOpen={isDumpAreaOpen} handleSideBar={handleSidebar} />
 			<DragDropContext onDragEnd={handleDragEnd}>
 				<StagingArea
-					handleSelectedTarget={(task) => handleSelectedTarget(task)}
-					selectedTarget={selectedTarget}
 					tasks={stagingData}
 					targetDate={targetDate}
 					isStagingOpen={isDumpAreaOpen}
@@ -166,8 +171,6 @@ function Today() {
 					<BtnTaskContainer type="target" />
 				) : (
 					<TargetArea
-						handleSelectedTarget={(task) => handleSelectedTarget(task)}
-						selectedTarget={selectedTarget}
 						tasks={targetData}
 						onClickPrevDate={handlePrevBtn}
 						onClickNextDate={handleNextBtn}
@@ -182,7 +185,6 @@ function Today() {
 			<CalendarWrapper>
 				<FullCalendarBox
 					size={isDumpAreaOpen ? 'small' : 'big'}
-					selectedTarget={selectedTarget}
 					selectDate={calenderSelectedDate}
 					handleChangeDate={handleChangeCalenderDate}
 				/>
diff --git a/src/store/useTaskSelectionStore.tsx b/src/store/useTaskSelectionStore.tsx
new file mode 100644
index 00000000..ea2ecefb
--- /dev/null
+++ b/src/store/useTaskSelectionStore.tsx
@@ -0,0 +1,21 @@
+import { create } from 'zustand';
+
+import { TaskType } from '@/types/tasks/taskType';
+
+interface TaskSelectionState {
+	selectedTask: TaskType | null;
+	setSelectedTask: (task: TaskType) => void;
+	clearSelectedTask: () => void;
+	isDragging: boolean;
+	setIsDragging: (dragging: boolean) => void;
+}
+
+const useTaskSelectionStore = create<TaskSelectionState>((set) => ({
+	selectedTask: null,
+	isDragging: false,
+	setSelectedTask: (task: TaskType) => set({ selectedTask: task }),
+	clearSelectedTask: () => set({ selectedTask: null }),
+	setIsDragging: (dragging: boolean) => set({ isDragging: dragging }),
+}));
+
+export default useTaskSelectionStore;
diff --git a/src/stories/Common/Button/Todo.stories.tsx b/src/stories/Common/Button/Todo.stories.tsx
index 52a18b90..67ba30e3 100644
--- a/src/stories/Common/Button/Todo.stories.tsx
+++ b/src/stories/Common/Button/Todo.stories.tsx
@@ -34,6 +34,10 @@ export const Default: Story = {
 		isStatusVisible: true,
 		taskId: 1,
 		targetDate: '2024년 12월 31일',
-		onClick: () => {},
+		task: {
+			id: 1,
+			name: 'Sample Task',
+			status: '미완료',
+		},
 	},
 };
diff --git a/src/types/area/taskAreaType.ts b/src/types/area/taskAreaType.ts
index d93498a4..36bb4ef8 100644
--- a/src/types/area/taskAreaType.ts
+++ b/src/types/area/taskAreaType.ts
@@ -3,8 +3,6 @@ import { TaskType } from '@/types/tasks/taskType';
 
 // 공통
 export interface BaseTaskAreaProps {
-	handleSelectedTarget: (task: TaskType | null) => void;
-	selectedTarget: TaskType | null;
 	tasks: TaskType[];
 	sortOrder: SortOrderType;
 	handleSortOrder: (order: SortOrderType) => void;
@@ -13,7 +11,7 @@ export interface BaseTaskAreaProps {
 
 // StagingArea 관련 속성
 export interface StagingAreaSettingProps {
-	isStagingOpen: boolean; 
+	isStagingOpen: boolean;
 }
 
 // TargetArea 관련 속성
diff --git a/yarn.lock b/yarn.lock
index 1d598c5b..e434fce5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5271,3 +5271,8 @@ yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zustand@^5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.3.tgz#b323435b73d06b2512e93c77239634374b0e407f"
+  integrity sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 13 out of 14 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (1)</summary>

* **package.json**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (3)</summary>

**src/components/targetArea/TargetTaskSection.tsx:10**
* [nitpick] The import name 'useTodoSelectionStore' is inconsistent with its usage elsewhere. Consider renaming it to 'useTaskSelectionStore' for consistency.
```
import useTodoSelectionStore from '@/store/useTaskSelectionStore';
```
**src/components/common/fullCalendar/FullCalendarBox.tsx:38**
* [nitpick] Inconsistent naming: consider using 'useTaskSelectionStore' instead of 'useTodoSelectionStore' to maintain consistency across the codebase.
```
const { selectedTask, clearSelectedTask, setIsDragging } = useTodoSelectionStore();
```
**src/components/common/StagingArea/StagingAreaTaskContainer.tsx:10**
* [nitpick] The naming of the imported store is inconsistent with other parts of the code. Consider renaming this import to 'useTaskSelectionStore'.
```
import useTodoSelectionStore from '@/store/useTaskSelectionStore';
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-06T12:06:08Z
user: login: seong-hui
body: task가 여러 곳에서 연관되어 사용되어지는만큼 클릭된 task관리를 위해서 전역상태 도입은 굉장히 좋은 선택인거 같아요! 확실히 선택한 task를 구별할 수 있으니 사용하면서 직관적이고 좋으네요! 드랍으로 캘린더에 일정을 생성하는 부분은 더 고민을 해보면 좋을 것 같습니다! 해당 기능이 없어도 서비스를 이용하기엔 무리가 없지만 약간 아쉽기도..? ㅋㅋㅋㅎ 이번에 어려우면 추후에 개발해도 좋을 것 같아요~~ 고생 많으셨습니다 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T11:30:02Z
user: login: Kjiw0n
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T02:32:50Z
user: login: Kjiw0n
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T08:10:56Z
user: login: seong-hui
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T08:26:00Z
user: login: Kjiw0n
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T09:07:39Z

Review comments:
diff_hunk: @@ -28,7 +28,8 @@
     "react-datepicker": "^7.3.0",
     "react-dom": "^18.3.1",
     "react-router-dom": "^6.24.1",
-    "vite-plugin-svgr": "^4.3.0"
+    "vite-plugin-svgr": "^4.3.0",
+    "zustand": "^5.0.3"
path: package.json
user: login: seong-hui
body: 전역상태관리를 라이브러리로 zustand를 선택하신 이유가 궁금해요! (정말 단순 궁금)
created_at: 2025-04-08T09:44:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,26 @@
+import { useEffect, RefObject } from 'react';
+
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
+
+const useOutsideTaskClick = (calendarRef: RefObject<HTMLElement>) => {
+	const { clearSelectedTask, selectedTask } = useTaskSelectionStore();
+
+	useEffect(() => {
+		const handleMouseDown = (event: MouseEvent) => {
path: src/hooks/useOutsideTaskClick.ts
user: login: seong-hui
body: 드롭을 통한 이벤트생성을 위해서 외부영역 클릭 시 clearSelectedTask()을 호출하기 전에 calendarRef.current 예외처리를 한것처럼 isDragging의 상태가 true이면 return되는 식으로 구현하는 건 어떨까요?
created_at: 2025-04-08T11:24:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+import { create } from 'zustand';
+
+import { TaskType } from '@/types/tasks/taskType';
+
+interface TaskSelectionState {
+	selectedTask: TaskType | null;
+	setSelectedTask: (task: TaskType) => void;
+	clearSelectedTask: () => void;
+	isDragging: boolean;
+	setIsDragging: (dragging: boolean) => void;
+}
+
+const useTaskSelectionStore = create<TaskSelectionState>((set) => ({
path: src/store/useTaskSelectionStore.tsx
user: login: seong-hui
body: zustand는 사용해보지 않았는데 이런식으로 이렇게 상태를 관리하는군요 직관적이고 좋으네용 👍 
created_at: 2025-04-08T11:27:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,26 @@
+import { useEffect, RefObject } from 'react';
+
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
+
+const useOutsideTaskClick = (calendarRef: RefObject<HTMLElement>) => {
+	const { clearSelectedTask, selectedTask } = useTaskSelectionStore();
+
+	useEffect(() => {
+		const handleMouseDown = (event: MouseEvent) => {
path: src/hooks/useOutsideTaskClick.ts
user: login: Kjiw0n
body: 그렇게 다룰 시 드래그가 엄청 딜레이되는 이슈가 있었는데, 보다 간단한 방식으로 해결되었습니다! 고민 감사합니다!! 
created_at: 2025-04-09T02:32:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,7 +28,8 @@
     "react-datepicker": "^7.3.0",
     "react-dom": "^18.3.1",
     "react-router-dom": "^6.24.1",
-    "vite-plugin-svgr": "^4.3.0"
+    "vite-plugin-svgr": "^4.3.0",
+    "zustand": "^5.0.3"
path: package.json
user: login: Kjiw0n
body: jotai랑 고민했는데, 러닝커브나 탑다운방식이 조금 더 익숙해서 선택했습니다! 선택된 테스크를 전체페이지에서 공유하는만큼 좀 더 어울린다고 생각하기도 했구요
created_at: 2025-04-09T08:10:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,26 @@
+import { useEffect, RefObject } from 'react';
+
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
+
+const useOutsideTaskClick = (calendarRef: RefObject<HTMLElement>) => {
+	const { clearSelectedTask, selectedTask } = useTaskSelectionStore();
+
+	useEffect(() => {
+		const handleMouseDown = (event: MouseEvent) => {
path: src/hooks/useOutsideTaskClick.ts
user: login: seong-hui
body: 오오 수정한 부분 확인했습니다. 다만 해당 수정과정에서 캘린더 영역의 task 더블 클릭이 막힌것 같아요 확인 부탁드립니다!!
created_at: 2025-04-09T08:26:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,26 @@
+import { useEffect, RefObject } from 'react';
+
+import useTaskSelectionStore from '@/store/useTaskSelectionStore';
+
+const useOutsideTaskClick = (calendarRef: RefObject<HTMLElement>) => {
+	const { clearSelectedTask, selectedTask } = useTaskSelectionStore();
+
+	useEffect(() => {
+		const handleMouseDown = (event: MouseEvent) => {
path: src/hooks/useOutsideTaskClick.ts
user: login: Kjiw0n
body: 헉 꼼꼼한 체크 감사합니다!!!👍 👍 👍  수정하였습니다
created_at: 2025-04-09T09:07:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-09T10:25:37Z
body: 🚀 Storybook 🚀
- https://673df30e5491debc7e4062eb-wfssytistu.chromatic.com/
author_association: NONE