209 lines
6.7 KiB
TypeScript
209 lines
6.7 KiB
TypeScript
|
|
"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";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========================================
|
||
|
|
// 브레이크포인트 (화면 너비 기준)
|
||
|
|
// ========================================
|
||
|
|
const BREAKPOINTS = {
|
||
|
|
// 모바일: 0 ~ 767px
|
||
|
|
// 태블릿: 768px 이상
|
||
|
|
TABLET_MIN: 768,
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 반응형 모드 자동 감지 훅
|
||
|
|
*
|
||
|
|
* - 화면 크기와 방향에 따라 4가지 모드 자동 전환
|
||
|
|
* - tablet_landscape, tablet_portrait, mobile_landscape, mobile_portrait
|
||
|
|
* - resize 이벤트와 orientation 변경 모두 감지
|
||
|
|
*
|
||
|
|
* @returns ResponsiveMode 객체
|
||
|
|
*/
|
||
|
|
export function useResponsiveMode(): ResponsiveMode {
|
||
|
|
const [mode, setMode] = useState<ResponsiveMode>({
|
||
|
|
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<boolean>(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<boolean | undefined>(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<DeviceType | undefined>(initialDeviceOverride);
|
||
|
|
const [orientationOverride, setOrientationOverride] = useState<boolean | undefined>(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,
|
||
|
|
};
|
||
|
|
}
|