CHORE(merge): merge from develop
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
CI / lint-and-build (push) Has been cancelled

- Initial setup and all features from develop branch
- Includes: auth, deploy, docker, style fixes
- K3S deployment configuration
This commit is contained in:
2026-01-06 17:29:16 +09:00
parent b4ce36ba3b
commit f78454c2a1
159 changed files with 18365 additions and 774 deletions

3
nextjs/hooks/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { useAuth } from "./useAuth";
export { useImageModal } from "./useImageModal";
export { usePagination } from "./usePagination";

65
nextjs/hooks/useAuth.ts Normal file
View 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;

View 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;

View 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;