refactor: 바깥클릭/ESC 닫기 공용 훅 useClickOutside 도입 (Phase 0)

11곳에 복붙되던 document 리스너 등록/해제·바깥클릭·ESC·스크롤 닫기 로직을
useClickOutside 훅으로 통합. 재사용 드롭다운 5종에 우선 적용.

- composables/useClickOutside.ts 신설 (event/esc/closeOnScroll 옵션)
- 적용: DropdownSelect, StatusSelect, ScheduleTypeFilter,
  ScheduleDetailPanel(참석자 스택), ScheduleEventModal(참석자 멀티셀렉트)
- 모달 ESC·페이지 레벨 오버레이 조정 로직은 후속 단계(BaseModal)에서 처리

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:31:45 +09:00
parent d5b54c1afb
commit 92ed6c332f
6 changed files with 79 additions and 75 deletions
@@ -0,0 +1,54 @@
import { onBeforeUnmount, onMounted, type Ref } from 'vue'
// 바깥 클릭 / ESC / 스크롤 시 닫기 공용 훅.
// 드롭다운·팝오버·메뉴 등에서 반복되던 document 리스너 등록/해제 로직을 한 곳으로 모은다.
//
// - isOpen: 열림 상태 ref (닫혀 있으면 아무 동작 안 함)
// - close: 닫기 콜백
// - containers: "내부"로 간주할 엘리먼트 ref 목록. 클릭이 이 안이면 무시한다
// (Teleport 로 body 에 띄운 메뉴는 트리거와 메뉴 ref 를 모두 넣는다)
// - opts.event: 바깥 클릭 감지 이벤트 (기본 'mousedown')
// - opts.esc: Escape 로 닫을지 (기본 true)
// - opts.closeOnScroll: 스크롤/리사이즈 시 닫을지 — fixed 로 띄운 메뉴 위치 어긋남 방지 (기본 false)
export function useClickOutside(
isOpen: Ref<boolean>,
close: () => void,
containers: Ref<HTMLElement | null>[],
opts: {
event?: 'mousedown' | 'click'
esc?: boolean
closeOnScroll?: boolean
} = {},
): void {
const { event = 'mousedown', esc = true, closeOnScroll = false } = opts
function onPointer(e: Event): void {
if (!isOpen.value) return
const t = e.target as Node
if (containers.some((r) => r.value?.contains(t))) return
close()
}
function onKey(e: KeyboardEvent): void {
if (isOpen.value && e.key === 'Escape') close()
}
function onScroll(): void {
if (isOpen.value) close()
}
onMounted(() => {
document.addEventListener(event, onPointer, true)
if (esc) document.addEventListener('keydown', onKey)
if (closeOnScroll) {
window.addEventListener('scroll', onScroll, true)
window.addEventListener('resize', onScroll)
}
})
onBeforeUnmount(() => {
document.removeEventListener(event, onPointer, true)
if (esc) document.removeEventListener('keydown', onKey)
if (closeOnScroll) {
window.removeEventListener('scroll', onScroll, true)
window.removeEventListener('resize', onScroll)
}
})
}