diff --git a/apps/web/next.config.js b/apps/web/next.config.js index fb780ced..8c62797f 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -41,6 +41,16 @@ const nextConfig = { source: '/api/users/me', destination: `${apiBaseUrl}/users/me`, }, + // 자유 게시판 + { + source: '/api/community/freeboard/:path*', + destination: `${apiBaseUrl}/community/freeboard/:path*`, + }, + // 투표 게시판 + { + source: '/api/community/votesboard/:path*', + destination: `${apiBaseUrl}/community/votesboard/:path*`, + }, ]; }, //추후 제거 필요 diff --git a/apps/web/src/app/main/community/freeboard/[freeboardId]/components/CommentInput.tsx b/apps/web/src/app/main/community/freeboard/[freeboardId]/components/CommentInput.tsx index 69fe9dd0..c57c2ee2 100644 --- a/apps/web/src/app/main/community/freeboard/[freeboardId]/components/CommentInput.tsx +++ b/apps/web/src/app/main/community/freeboard/[freeboardId]/components/CommentInput.tsx @@ -32,7 +32,7 @@ export default function CommentInput({ const targetRef = useRef(null); const queryClient = useQueryClient(); const toast = useToast(); - const { guard } = useAuthGuard(); + const { requireAuth } = useAuthGuard(); const { mutate, isPending } = useMutation({ mutationFn: (content: string) => @@ -67,11 +67,10 @@ export default function CommentInput({ setValue(next.length > limit ? next.slice(0, limit) : next); }; - const handleSubmit = () => - guard(() => { - if (!value.trim() || isPending) return; - mutate(value.trim()); - }); + const handleSubmit = requireAuth(() => { + if (!value.trim() || isPending) return; + mutate(value.trim()); + }); const handleKeyDown = ( e: React.KeyboardEvent, diff --git a/apps/web/src/app/main/community/freeboard/[freeboardId]/components/LikeButtonComment.tsx b/apps/web/src/app/main/community/freeboard/[freeboardId]/components/LikeButtonComment.tsx index 42dd6b8d..1a123473 100644 --- a/apps/web/src/app/main/community/freeboard/[freeboardId]/components/LikeButtonComment.tsx +++ b/apps/web/src/app/main/community/freeboard/[freeboardId]/components/LikeButtonComment.tsx @@ -32,7 +32,7 @@ export default function LikeButtonComment({ }: LikeButtonCommentProps) { const queryClient = useQueryClient(); const toast = useToast(); - const { guard } = useAuthGuard(); + const { requireAuth } = useAuthGuard(); // UI 전용 상태(부모 props와 동기화됨) const [liked, setLiked] = useState(!!initialLiked); @@ -95,17 +95,16 @@ export default function LikeButtonComment({ }); // 클릭 시: 가드 통과 후, 중복 요청 방지 & 뮤테이션 트리거 - const handleToggleLike = () => - guard(() => { - if (toggleLike.isPending) return; - toggleLike.mutate({ freeboardId: postId, commentId }); - }); + const handleToggleLike = () => { + if (toggleLike.isPending) return; + toggleLike.mutate({ freeboardId: postId, commentId }); + }; return ( + + + + + + + ); +} diff --git a/apps/web/src/hooks/useAuth.ts b/apps/web/src/hooks/useAuth.ts index eb18ff12..74bff97e 100644 --- a/apps/web/src/hooks/useAuth.ts +++ b/apps/web/src/hooks/useAuth.ts @@ -5,8 +5,10 @@ import { } from '@/generated/api/endpoints/users/users'; import { ApiError } from '@/lib/api-error'; import type { UserResponse } from '@/generated/api/models'; -import { useToast } from './ui/useToast'; import { useLogout } from './useLogout'; +import { useOverlay } from './ui/useOverlay'; +import React from 'react'; +import { LoginRedirectOverlay } from '@/components/LoginRedirectOverlay'; /** * 현재 로그인한 사용자 정보를 조회하는 Hook @@ -83,60 +85,61 @@ export function useAuthRestore() { } /** - * 인증 가드 Hook (기존 코드 호환성 유지) - * - * ## 반환값 - * - `authed`: 인증 여부 (boolean) - * - `guard(fn)`: 인증된 경우에만 fn 실행, 아니면 토스트 표시 - * - `ensureAuthed()`: 인증 여부 체크, false면 토스트 표시 - * - * ## 레거시 호환 - * 기존 코드에서 사용하던 guard/ensureAuthed 패턴을 유지합니다. - * 내부적으로는 새로운 useAuth()를 사용하여 SSR 최적화를 활용합니다. + * 행동 단위 인증 가드 Hook (HOF 패턴) * + * - 인증된 상태: 넘겨준 액션(fn)을 그대로 실행 + * - 비인증 상태: 로그인 리다이렉트 오버레이를 띄우고, 액션은 실행하지 않음 */ -export function useAuthGuard( - options: { onUnauthed?: () => void } = {}, -) { +export function useAuthGuard() { + // 현재 로그인 여부 const { isAuth } = useAuth(); - const toast = useToast(); - - // 비로그인 기본 처리: 토스트 표시 (기존 동작 유지) - // 옵션으로 커스텀 동작 지정 가능 (예: 리다이렉트) - const onUnauthed = - options.onUnauthed ?? - (() => toast('로그인이 필요합니다.', 'error')); + // 전역 오버레이 제어 훅 + const { open } = useOverlay(); /** - * 인증된 경우에만 함수 실행 - * @param fn - 실행할 함수 + * 내부 헬퍼: "지금 로그인 되어 있는지" 확인하는 함수 + * + * - 로그인 X: + * - LoginRedirectOverlay 오버레이를 띄움 + * - false 반환 + * - 로그인 O: + * - true 반환 */ - const guard = (fn: () => void | Promise) => { + const ensureAuthed = async () => { if (!isAuth) { - onUnauthed(); - return; - } - return fn(); - }; - - /** - * 인증 여부 확인 (조기 리턴 패턴에 사용) - * @returns 인증 여부 - */ - const ensureAuthed = () => { - if (!isAuth) { - onUnauthed(); + // 오버레이 스택에 로그인 리다이렉트 모달 추가 + await open( + // renderer: close 함수를 받아서 오버레이 컴포넌트를 렌더링 + ({ close }) => + React.createElement(LoginRedirectOverlay, { close }), + { + blockScroll: true, + closeOnBackdrop: true, + }, + ); return false; } return true; }; + /** + * 고차함수(HOF) 패턴 + * + * - 인자: fn: 실제로 실행하고 싶은 액션(함수) + * - 반환: 로그인 체크가 래핑된 새 함수 + **/ + const requireAuth = + // Args: 원래 함수가 받을 인자 타입들 + ( + fn: (...args: Args) => void | Promise, + ) => + async (...args: Args) => { + if (!(await ensureAuthed())) return; + return fn(...args); + }; + return { - /** 인증 여부 */ authed: isAuth, - /** 인증된 경우에만 fn 실행 */ - guard, - /** 인증 여부 확인 후 false면 onUnauthed 실행 */ - ensureAuthed, + requireAuth, }; } diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 49c192dd..0a94fd4d 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -6,7 +6,12 @@ import { refreshToken } from '@/generated/api/endpoints/auth/auth'; import { ApiError } from './api-error'; // 쿠키가 필요한 경로 (프록시 사용) -const COOKIE_REQUIRED_PATHS = ['/auth/', '/users/me']; +const COOKIE_REQUIRED_PATHS = [ + '/auth/', + '/users/me', + '/community/freeboard/', + '/community/votesboard/', +]; export const AXIOS_INSTANCE = Axios.create({ baseURL: @@ -26,6 +31,12 @@ export const AXIOS_INSTANCE = Axios.create({ AXIOS_INSTANCE.interceptors.request.use((config) => { const proxyEnabled = process.env.NEXT_PUBLIC_ENABLE_PROXY !== 'false'; + const isBrowser = typeof window !== 'undefined'; + + // SSR에서는 절대 URL 그대로 사용 (상대 경로로 바꾸면 Invalid URL 발생) + if (!isBrowser) { + return config; + } // 프록시 비활성화 시 직접 백엔드 호출 if (!proxyEnabled) {