diff --git a/src/components/Dropdown/AlarmDropdownContent.tsx b/src/components/Dropdown/AlarmDropdownContent.tsx
new file mode 100644
index 0000000..633e1ff
--- /dev/null
+++ b/src/components/Dropdown/AlarmDropdownContent.tsx
@@ -0,0 +1,56 @@
+import { useEffect } from "react";
+
+import { useAlarmStore } from "@/store/useAlarmStore";
+
+export default function AlarmDropdownContent({
+ onClose,
+}: {
+ onClose: () => void;
+}) {
+ const { alarms, markAllAsRead } = useAlarmStore();
+
+ useEffect(() => {
+ markAllAsRead();
+ }, [markAllAsRead]);
+
+ return (
+
+
+ 알림 {alarms.length}개
+
+
+
+ {alarms.length === 0 ? (
+
알림이 없습니다.
+ ) : (
+
+ {alarms.map((alarm) => (
+ - {
+ onClose();
+ }}
+ >
+
+
+
+ {alarm.message}{" "}
+
+ {alarm.status}
+
+ 되었어요.
+
+
+ {alarm.createdAt}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Dropdown/DropdownPopover.tsx b/src/components/Dropdown/DropdownPopover.tsx
new file mode 100644
index 0000000..073edd0
--- /dev/null
+++ b/src/components/Dropdown/DropdownPopover.tsx
@@ -0,0 +1,108 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+
+import clsx from "clsx";
+import ReactDOM from "react-dom";
+
+interface DropdownPopoverProps {
+ anchorRef: React.RefObject;
+ onClose: () => void;
+ align?: "left" | "right";
+ variant?: "filter" | "alarm";
+ children: React.ReactNode;
+}
+
+const DropdownPopover = ({
+ anchorRef,
+ onClose,
+ align = "left",
+ variant = "filter",
+ children,
+}: DropdownPopoverProps) => {
+ const popoverRef = useRef(null);
+ const [position, setPosition] = useState({ top: 0, left: 0 });
+
+ // 위치 계산 함수
+ const calculatePosition = useCallback(() => {
+ const anchor = anchorRef.current;
+ const isMobile = window.innerWidth < 768;
+ const POPUP_WIDTH = variant === "alarm" ? 368 : 390;
+
+ if (!anchor) return;
+
+ const rect = anchor.getBoundingClientRect();
+
+ if (isMobile) {
+ setPosition({ top: 0, left: 0 });
+ } else {
+ setPosition({
+ top: rect.bottom + window.scrollY + 8,
+ left:
+ align === "right"
+ ? rect.right + window.scrollX - POPUP_WIDTH
+ : rect.left + window.scrollX,
+ });
+ }
+ }, [anchorRef, align, variant]);
+
+ // 창 크기 변경 대응
+ useEffect(() => {
+ calculatePosition();
+ window.addEventListener("resize", calculatePosition);
+ return () => {
+ window.removeEventListener("resize", calculatePosition);
+ };
+ }, [calculatePosition]);
+
+ // 클릭 외부 감지 → 드롭다운 닫기
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ if (
+ popoverRef.current &&
+ !popoverRef.current.contains(e.target as Node) &&
+ anchorRef.current &&
+ !anchorRef.current.contains(e.target as Node)
+ ) {
+ onClose();
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [anchorRef, onClose]);
+
+ const isFilter = variant === "filter";
+
+ const sizeClass = isFilter
+ ? "md:w-[24.375rem] md:h-[52.75rem]"
+ : "md:w-[23rem] md:h-[26.25rem]";
+
+ const colorClass = isFilter
+ ? "bg-white border-gray-200"
+ : "bg-red-50 border-gray-300";
+
+ const popoverClass = clsx(
+ "z-[9999]",
+ "rounded-2xl p-6 pr-5 pb-6 pl-5",
+ "shadow-[0px_2px_8px_0px_#78748640]",
+ "border fixed md:absolute w-full h-full left-0 top-0",
+ colorClass,
+ sizeClass,
+ );
+
+ return ReactDOM.createPortal(
+
+ {children}
+
,
+ document.body,
+ );
+};
+
+export default DropdownPopover;
diff --git a/src/components/Dropdown/FilterDropdownContent.tsx b/src/components/Dropdown/FilterDropdownContent.tsx
new file mode 100644
index 0000000..895ee78
--- /dev/null
+++ b/src/components/Dropdown/FilterDropdownContent.tsx
@@ -0,0 +1,125 @@
+import Button from "@/components/Button";
+import { useFilterStore } from "@/store/useFilterStore";
+import { SeoulDistricts } from "@/types/common";
+
+function FilterDropdownContent({ onClose }: { onClose: () => void }) {
+ const {
+ selectedAreas,
+ startDate,
+ minPay,
+ setAreas,
+ setStartDate,
+ setMinPay,
+ reset,
+ } = useFilterStore();
+
+ const toggleArea = (area: string) => {
+ const updated = selectedAreas.includes(area)
+ ? selectedAreas.filter((a) => a !== area)
+ : [...selectedAreas, area];
+ setAreas(updated);
+ };
+
+ return (
+
+
+ 상세 필터
+
+
+
+
+
위치
+
+ {SeoulDistricts.map((area) => (
+
+ ))}
+
+
+ {selectedAreas.length > 0 && (
+
+ {selectedAreas.map((area) => (
+
+ {area}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
시작일
+
setStartDate(e.target.value)}
+ className="w-full h-[3.625rem] border border-gray-300 rounded-md px-4 text-base text-black placeholder:text-gray-400 bg-white"
+ placeholder="입력"
+ />
+
+
+
+
+
+
금액
+
+
+ setMinPay(Number(e.target.value))}
+ className="w-[10.5rem] h-[3.625rem] border border-gray-300 rounded-md px-4 pr-[2.5rem] text-base text-black placeholder:text-gray-400"
+ placeholder="입력"
+ />
+
+ 원
+
+
+
이상부터
+
+
+
+
+
+
+
+
+ );
+}
+
+export default FilterDropdownContent;
diff --git a/src/store/useAlarmStore.ts b/src/store/useAlarmStore.ts
new file mode 100644
index 0000000..53525c9
--- /dev/null
+++ b/src/store/useAlarmStore.ts
@@ -0,0 +1,30 @@
+import { create } from "zustand";
+
+export type Alarm = {
+ id: string;
+ message: string;
+ createdAt: string;
+ read: boolean;
+ status: "승인" | "거절";
+};
+
+interface AlarmStore {
+ alarms: Alarm[];
+ hasUnread: boolean;
+ setAlarms: (alarms: Alarm[]) => void;
+ markAllAsRead: () => void;
+}
+
+export const useAlarmStore = create((set, get) => ({
+ alarms: [],
+ hasUnread: false,
+ setAlarms: (alarms) =>
+ set({
+ alarms,
+ hasUnread: alarms.some((a) => !a.read),
+ }),
+ markAllAsRead: () => {
+ const updated = get().alarms.map((a) => ({ ...a, read: true }));
+ set({ alarms: updated, hasUnread: false });
+ },
+}));
diff --git a/src/store/useFilterStore.ts b/src/store/useFilterStore.ts
new file mode 100644
index 0000000..a35fb45
--- /dev/null
+++ b/src/store/useFilterStore.ts
@@ -0,0 +1,28 @@
+import { create } from "zustand";
+
+interface FilterState {
+ selectedAreas: string[];
+ startDate: string | null;
+ minPay: number | null;
+ setAreas: (areas: string[]) => void;
+ setStartDate: (date: string) => void;
+ setMinPay: (pay: number | null) => void;
+ reset: () => void;
+}
+
+export const useFilterStore = create((set) => ({
+ selectedAreas: [],
+ startDate: null,
+ minPay: null,
+
+ setAreas: (areas) => set({ selectedAreas: areas }),
+ setStartDate: (date) => set({ startDate: date }),
+ setMinPay: (pay) => set({ minPay: pay }),
+
+ reset: () =>
+ set({
+ selectedAreas: [],
+ startDate: null,
+ minPay: null,
+ }),
+}));