"use client"; import { useState, useEffect, useMemo } from "react"; // ======================================== // 타입 정의 // ======================================== export type DeviceType = "mobile" | "tablet"; export type OrientationType = "landscape" | "portrait"; export interface ResponsiveMode { device: DeviceType; orientation: OrientationType; isLandscape: boolean; modeKey: "tablet_landscape" | "tablet_portrait" | "mobile_landscape" | "mobile_portrait"; } // ======================================== // 브레이크포인트 (화면 너비 기준) // GRID_BREAKPOINTS와 일치해야 함! // ======================================== const BREAKPOINTS = { // mobile_portrait: ~479px (4칸) // mobile_landscape: 480~767px (6칸) // tablet_portrait: 768~1023px (8칸) // tablet_landscape: 1024px~ (12칸) TABLET_MIN: 768, // 768px 이상이면 tablet }; /** * 반응형 모드 자동 감지 훅 * * - 화면 크기와 방향에 따라 4가지 모드 자동 전환 * - tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait * - resize 이벤트와 orientation 변경 모두 감지 * * @returns ResponsiveMode 객체 */ export function useResponsiveMode(): ResponsiveMode { const [mode, setMode] = useState({ device: "tablet", orientation: "landscape", isLandscape: true, modeKey: "tablet_landscape", }); useEffect(() => { if (typeof window === "undefined") return; const detectMode = (): ResponsiveMode => { const width = window.innerWidth; const height = window.innerHeight; // 디바이스 타입 결정 (화면 너비 기준) const device: DeviceType = width >= BREAKPOINTS.TABLET_MIN ? "tablet" : "mobile"; // 방향 결정 (가로/세로 비율) const isLandscape = width > height; const orientation: OrientationType = isLandscape ? "landscape" : "portrait"; // 모드 키 생성 const modeKey = `${device}_${orientation}` as ResponsiveMode["modeKey"]; return { device, orientation, isLandscape, modeKey }; }; // 초기값 설정 setMode(detectMode()); const handleChange = () => { setTimeout(() => { setMode(detectMode()); }, 100); }; // 이벤트 리스너 등록 window.addEventListener("resize", handleChange); window.addEventListener("orientationchange", handleChange); // matchMedia로 orientation 변경 감지 const landscapeQuery = window.matchMedia("(orientation: landscape)"); if (landscapeQuery.addEventListener) { landscapeQuery.addEventListener("change", handleChange); } return () => { window.removeEventListener("resize", handleChange); window.removeEventListener("orientationchange", handleChange); if (landscapeQuery.removeEventListener) { landscapeQuery.removeEventListener("change", handleChange); } }; }, []); return mode; } /** * 디바이스 방향(orientation) 감지 커스텀 훅 * * - 실제 디바이스에서 가로/세로 방향 변경을 감지 * - window.matchMedia와 orientationchange 이벤트 활용 * - SSR 호환성 고려 (typeof window !== 'undefined') * * @returns isLandscape - true: 가로 모드, false: 세로 모드 */ export function useDeviceOrientation(): boolean { const [isLandscape, setIsLandscape] = useState(false); useEffect(() => { if (typeof window === "undefined") return; const detectOrientation = (): boolean => { if (window.matchMedia) { const landscapeQuery = window.matchMedia("(orientation: landscape)"); return landscapeQuery.matches; } return window.innerWidth > window.innerHeight; }; setIsLandscape(detectOrientation()); const handleOrientationChange = () => { setTimeout(() => { setIsLandscape(detectOrientation()); }, 100); }; const landscapeQuery = window.matchMedia("(orientation: landscape)"); if (landscapeQuery.addEventListener) { landscapeQuery.addEventListener("change", handleOrientationChange); } else if (landscapeQuery.addListener) { landscapeQuery.addListener(handleOrientationChange); } window.addEventListener("orientationchange", handleOrientationChange); window.addEventListener("resize", handleOrientationChange); return () => { if (landscapeQuery.removeEventListener) { landscapeQuery.removeEventListener("change", handleOrientationChange); } else if (landscapeQuery.removeListener) { landscapeQuery.removeListener(handleOrientationChange); } window.removeEventListener("orientationchange", handleOrientationChange); window.removeEventListener("resize", handleOrientationChange); }; }, []); return isLandscape; } /** * 수동 방향 전환을 지원하는 확장 훅 * 프리뷰 모드에서 테스트 목적으로 사용 * * @param initialOverride - 초기 수동 설정값 (undefined면 자동 감지) * @returns [isLandscape, setIsLandscape, isAutoDetect] */ export function useDeviceOrientationWithOverride( initialOverride?: boolean ): [boolean, (value: boolean | undefined) => void, boolean] { const autoDetectedIsLandscape = useDeviceOrientation(); const [manualOverride, setManualOverride] = useState(initialOverride); const isLandscape = manualOverride !== undefined ? manualOverride : autoDetectedIsLandscape; const isAutoDetect = manualOverride === undefined; const setOrientation = (value: boolean | undefined) => { setManualOverride(value); }; return [isLandscape, setOrientation, isAutoDetect]; } /** * 반응형 모드 + 수동 오버라이드 지원 훅 * 프리뷰 모드에서 디바이스/방향을 수동으로 변경할 때 사용 */ export function useResponsiveModeWithOverride( initialDeviceOverride?: DeviceType, initialOrientationOverride?: boolean ): { mode: ResponsiveMode; setDevice: (device: DeviceType | undefined) => void; setOrientation: (isLandscape: boolean | undefined) => void; isAutoDetect: boolean; } { const autoMode = useResponsiveMode(); const [deviceOverride, setDeviceOverride] = useState(initialDeviceOverride); const [orientationOverride, setOrientationOverride] = useState(initialOrientationOverride); const mode = useMemo((): ResponsiveMode => { const device = deviceOverride ?? autoMode.device; const isLandscape = orientationOverride ?? autoMode.isLandscape; const orientation: OrientationType = isLandscape ? "landscape" : "portrait"; const modeKey = `${device}_${orientation}` as ResponsiveMode["modeKey"]; return { device, orientation, isLandscape, modeKey }; }, [autoMode, deviceOverride, orientationOverride]); const isAutoDetect = deviceOverride === undefined && orientationOverride === undefined; return { mode, setDevice: setDeviceOverride, setOrientation: setOrientationOverride, isAutoDetect, }; }