CHORE(merge): merge from develop
- Initial setup and all features from develop branch - Includes: auth, deploy, docker, style fixes - K3S deployment configuration
This commit is contained in:
3
nextjs/hooks/index.ts
Normal file
3
nextjs/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useAuth } from "./useAuth";
|
||||
export { useImageModal } from "./useImageModal";
|
||||
export { usePagination } from "./usePagination";
|
||||
65
nextjs/hooks/useAuth.ts
Normal file
65
nextjs/hooks/useAuth.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getMe, type User } from "@/lib/services";
|
||||
|
||||
interface UseAuthOptions {
|
||||
/** 인증 실패 시 리다이렉트할 경로 */
|
||||
redirectTo?: string;
|
||||
/** 인증 필수 여부 (false면 인증 실패해도 에러 안 던짐) */
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface UseAuthReturn {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
checkAuth: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 상태를 관리하는 훅
|
||||
*
|
||||
* @example
|
||||
* // 기본 사용 (인증 선택적)
|
||||
* const { user, isLoading, isAuthenticated } = useAuth();
|
||||
*
|
||||
* @example
|
||||
* // 인증 필수 페이지
|
||||
* const { user, isLoading } = useAuth({
|
||||
* required: true,
|
||||
* redirectTo: '/login'
|
||||
* });
|
||||
*/
|
||||
export function useAuth(options: UseAuthOptions = {}): UseAuthReturn {
|
||||
const { redirectTo, required = false } = options;
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const checkAuth = useCallback(async () => {
|
||||
try {
|
||||
const userData = await getMe();
|
||||
setUser(userData);
|
||||
} catch {
|
||||
setUser(null);
|
||||
if (required && redirectTo && typeof window !== "undefined") {
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [required, redirectTo]);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
checkAuth,
|
||||
};
|
||||
}
|
||||
|
||||
export default useAuth;
|
||||
123
nextjs/hooks/useImageModal.ts
Normal file
123
nextjs/hooks/useImageModal.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
interface UseImageModalReturn {
|
||||
selectedIndex: number | null;
|
||||
isOpen: boolean;
|
||||
open: (index: number) => void;
|
||||
close: () => void;
|
||||
next: () => void;
|
||||
prev: () => void;
|
||||
goTo: (index: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 갤러리 모달을 관리하는 훅
|
||||
*
|
||||
* @param totalImages 전체 이미지 개수
|
||||
* @param options 추가 옵션
|
||||
*
|
||||
* @example
|
||||
* const { selectedIndex, isOpen, open, close, next, prev } = useImageModal(images.length);
|
||||
*
|
||||
* // 이미지 클릭 시
|
||||
* <div onClick={() => open(index)}>...</div>
|
||||
*
|
||||
* // 모달에서
|
||||
* {isOpen && (
|
||||
* <Modal onClose={close}>
|
||||
* <button onClick={prev}>이전</button>
|
||||
* <img src={images[selectedIndex].url} />
|
||||
* <button onClick={next}>다음</button>
|
||||
* </Modal>
|
||||
* )}
|
||||
*/
|
||||
export function useImageModal(
|
||||
totalImages: number,
|
||||
options: {
|
||||
loop?: boolean;
|
||||
enableKeyboard?: boolean;
|
||||
} = {}
|
||||
): UseImageModalReturn {
|
||||
const { loop = true, enableKeyboard = true } = options;
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
|
||||
const isOpen = selectedIndex !== null;
|
||||
|
||||
const open = useCallback((index: number) => {
|
||||
setSelectedIndex(index);
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setSelectedIndex(null);
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
if (selectedIndex === null || totalImages === 0) return;
|
||||
|
||||
if (loop) {
|
||||
setSelectedIndex((prev) =>
|
||||
prev !== null && prev < totalImages - 1 ? prev + 1 : 0
|
||||
);
|
||||
} else {
|
||||
setSelectedIndex((prev) =>
|
||||
prev !== null && prev < totalImages - 1 ? prev + 1 : prev
|
||||
);
|
||||
}
|
||||
}, [selectedIndex, totalImages, loop]);
|
||||
|
||||
const prev = useCallback(() => {
|
||||
if (selectedIndex === null || totalImages === 0) return;
|
||||
|
||||
if (loop) {
|
||||
setSelectedIndex((prev) =>
|
||||
prev !== null && prev > 0 ? prev - 1 : totalImages - 1
|
||||
);
|
||||
} else {
|
||||
setSelectedIndex((prev) =>
|
||||
prev !== null && prev > 0 ? prev - 1 : prev
|
||||
);
|
||||
}
|
||||
}, [selectedIndex, totalImages, loop]);
|
||||
|
||||
const goTo = useCallback((index: number) => {
|
||||
if (index >= 0 && index < totalImages) {
|
||||
setSelectedIndex(index);
|
||||
}
|
||||
}, [totalImages]);
|
||||
|
||||
// 키보드 네비게이션
|
||||
useEffect(() => {
|
||||
if (!enableKeyboard || !isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
prev();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
next();
|
||||
break;
|
||||
case "Escape":
|
||||
close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [enableKeyboard, isOpen, next, prev, close]);
|
||||
|
||||
return {
|
||||
selectedIndex,
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
next,
|
||||
prev,
|
||||
goTo,
|
||||
};
|
||||
}
|
||||
|
||||
export default useImageModal;
|
||||
97
nextjs/hooks/usePagination.ts
Normal file
97
nextjs/hooks/usePagination.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
interface UsePaginationReturn {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
setCurrentPage: (page: number) => void;
|
||||
setTotalPages: (total: number) => void;
|
||||
nextPage: () => void;
|
||||
prevPage: () => void;
|
||||
goToPage: (page: number) => void;
|
||||
isFirstPage: boolean;
|
||||
isLastPage: boolean;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
interface UsePaginationOptions {
|
||||
initialPage?: number;
|
||||
initialTotalPages?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지네이션 상태를 관리하는 훅
|
||||
*
|
||||
* @example
|
||||
* const {
|
||||
* currentPage,
|
||||
* totalPages,
|
||||
* setCurrentPage,
|
||||
* setTotalPages,
|
||||
* nextPage,
|
||||
* prevPage
|
||||
* } = usePagination();
|
||||
*
|
||||
* // 데이터 로드 시
|
||||
* const result = await fetchData(currentPage);
|
||||
* setTotalPages(result.pagination.totalPages);
|
||||
*
|
||||
* // Pagination 컴포넌트와 함께
|
||||
* <Pagination
|
||||
* currentPage={currentPage}
|
||||
* totalPages={totalPages}
|
||||
* onPageChange={setCurrentPage}
|
||||
* />
|
||||
*/
|
||||
export function usePagination(
|
||||
options: UsePaginationOptions = {}
|
||||
): UsePaginationReturn {
|
||||
const { initialPage = 1, initialTotalPages = 1 } = options;
|
||||
|
||||
const [currentPage, setCurrentPageState] = useState(initialPage);
|
||||
const [totalPages, setTotalPages] = useState(initialTotalPages);
|
||||
|
||||
const setCurrentPage = useCallback((page: number) => {
|
||||
if (page >= 1) {
|
||||
setCurrentPageState(page);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
setCurrentPageState((prev) => (prev < totalPages ? prev + 1 : prev));
|
||||
}, [totalPages]);
|
||||
|
||||
const prevPage = useCallback(() => {
|
||||
setCurrentPageState((prev) => (prev > 1 ? prev - 1 : prev));
|
||||
}, []);
|
||||
|
||||
const goToPage = useCallback(
|
||||
(page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
setCurrentPageState(page);
|
||||
}
|
||||
},
|
||||
[totalPages]
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setCurrentPageState(initialPage);
|
||||
setTotalPages(initialTotalPages);
|
||||
}, [initialPage, initialTotalPages]);
|
||||
|
||||
return {
|
||||
currentPage,
|
||||
totalPages,
|
||||
setCurrentPage,
|
||||
setTotalPages,
|
||||
nextPage,
|
||||
prevPage,
|
||||
goToPage,
|
||||
isFirstPage: currentPage === 1,
|
||||
isLastPage: currentPage === totalPages,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePagination;
|
||||
Reference in New Issue
Block a user