Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/app/(customer)/(protected)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@
import type { ReactNode } from "react";
import { usePathname } from "next/navigation";

import ProfileCompletionGuard from "@/components/auth/ProfileCompletionGuard";
import RoleGuard from "@/components/auth/RoleGuard";
import { getCustomerProtectedLoadingFallback } from "@/lib/loading/getCustomerProtectedLoadingFallback";

interface CustomerProtectedLayoutProps {
children: ReactNode;
}

/**
* 고객 `(protected)` Route Group 공통 가드.
* 프로필 완료 검사는 이 layout의 ProfileCompletionGuard에서만 처리한다.
* 하위 페이지에 CustomerAuthGate(+ Guard)를 추가로 감싸지 말 것.
*/
const CustomerProtectedLayout = ({ children }: CustomerProtectedLayoutProps) => {
const pathname = usePathname();
const loadingFallback = getCustomerProtectedLoadingFallback(pathname);

return (
<RoleGuard
allowedRole="CUSTOMER"
loadingFallback={getCustomerProtectedLoadingFallback(pathname)}
>
{children}
<RoleGuard allowedRole="CUSTOMER" loadingFallback={loadingFallback}>
Comment thread
9g-g9 marked this conversation as resolved.
<ProfileCompletionGuard loadingFallback={loadingFallback}>{children}</ProfileCompletionGuard>
</RoleGuard>
);
};
Expand Down
11 changes: 9 additions & 2 deletions src/app/(mover)/(protected)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@
import type { ReactNode } from "react";
import { usePathname } from "next/navigation";

import ProfileCompletionGuard from "@/components/auth/ProfileCompletionGuard";
import RoleGuard from "@/components/auth/RoleGuard";
import { getMoverProtectedLoadingFallback } from "@/lib/loading/getMoverProtectedLoadingFallback";

interface MoverProtectedLayoutProps {
children: ReactNode;
}

/**
* 기사 `(protected)` Route Group 공통 가드.
* 프로필 완료 검사는 이 layout의 ProfileCompletionGuard에서만 처리한다.
* 하위 페이지에 MoverAuthGate(+ Guard)를 추가로 감싸지 말 것.
*/
const MoverProtectedLayout = ({ children }: MoverProtectedLayoutProps) => {
const pathname = usePathname();
const loadingFallback = getMoverProtectedLoadingFallback(pathname);

return (
<RoleGuard allowedRole="MOVER" loadingFallback={getMoverProtectedLoadingFallback(pathname)}>
{children}
<RoleGuard allowedRole="MOVER" loadingFallback={loadingFallback}>
<ProfileCompletionGuard loadingFallback={loadingFallback}>{children}</ProfileCompletionGuard>
</RoleGuard>
);
};
Expand Down
9 changes: 6 additions & 3 deletions src/app/estimate-request/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import CustomerAuthGate from "@/components/auth/CustomerAuthGate";
import EstimateRequestForm from "@/components/estimate/request/EstimateRequestForm";

export default function EstimateRequestPage() {
return (
<main className="bg-background-subtle min-h-screen md:px-40 md:py-64">
<EstimateRequestForm />
</main>
<CustomerAuthGate loadingMessage="견적 요청을 준비하는 중입니다.">
<main className="bg-background-subtle min-h-screen md:px-40 md:py-64">
<EstimateRequestForm />
</main>
</CustomerAuthGate>
Comment thread
9g-g9 marked this conversation as resolved.
);
}
9 changes: 6 additions & 3 deletions src/app/estimate/received-requests/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import MoverAuthGate from "@/components/auth/MoverAuthGate";
import ReceivedRequestsPage from "@/components/estimate/ReceivedRequestsPage";

export default function Page() {
return (
<div className="bg-background-default text-text-primary min-h-screen">
<ReceivedRequestsPage />
</div>
<MoverAuthGate loadingMessage="받은 요청을 불러오는 중입니다.">
<div className="bg-background-default text-text-primary min-h-screen">
<ReceivedRequestsPage />
</div>
</MoverAuthGate>
);
}
7 changes: 6 additions & 1 deletion src/app/estimate/sent/[estimateId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";

import MoverAuthGate from "@/components/auth/MoverAuthGate";
import SentEstimateDetailPage from "@/components/estimate/sent/SentEstimateDetailPage";

export const metadata: Metadata = {
Expand All @@ -20,5 +21,9 @@ export default async function SentEstimateDetailRoute({ params }: PageProps) {
notFound();
}

return <SentEstimateDetailPage estimateId={estimateId} />;
return (
<MoverAuthGate loadingMessage="보낸 견적을 불러오는 중입니다.">
<SentEstimateDetailPage estimateId={estimateId} />
</MoverAuthGate>
);
}
7 changes: 6 additions & 1 deletion src/app/estimate/sent/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";

import MoverAuthGate from "@/components/auth/MoverAuthGate";
import SentEstimatesPage from "@/components/estimate/sent/SentEstimatesPage";

export const metadata: Metadata = {
Expand All @@ -8,5 +9,9 @@ export const metadata: Metadata = {
};

export default function SentEstimatesRoute() {
return <SentEstimatesPage />;
return (
<MoverAuthGate loadingMessage="보낸 견적을 불러오는 중입니다.">
<SentEstimatesPage />
</MoverAuthGate>
);
}
19 changes: 17 additions & 2 deletions src/components/auth/CustomerAuthGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { usePathname, useRouter } from "next/navigation";
import { useEffect, type ReactNode } from "react";

import ProfileCompletionGuard from "@/components/auth/ProfileCompletionGuard";
import EstimatesQueryStatus from "@/components/estimate/EstimatesQueryStatus";
import { useCustomerAuthReady } from "@/hooks/useCustomerAuthReady";
import { buildLoginPath, getRoleHomePath } from "@/lib/auth/redirect";
Expand All @@ -20,6 +21,12 @@ interface CustomerAuthGateProps {
* - 세션 복구 중: 로딩
* - 비로그인: 로그인 페이지로 이동 (?redirect=)
* - CUSTOMER 아님(기사님·ADMIN·역할 미확정): getRoleHomePath로 이동
* - 프로필 미완료: ProfileCompletionGuard (모달)
*
* `(customer)/(protected)` layout 밖 페이지에서만 사용한다.
* layout 안에서는 RoleGuard + layout의 ProfileCompletionGuard만 쓰고,
* 이 Gate를 겹쳐 두지 않는다.
*
* // 2026.07.30 정슬기 - [추가]
* // 2026.07.30 정슬기 - [수정] 리다이렉트를 router.replace로 통일 (하드 새로고침 불필요)
* // 2026.08.03 정슬기 - [수정] CUSTOMER 명시 판별로 canFetch·비고객 리다이렉트
Expand Down Expand Up @@ -53,9 +60,17 @@ export default function CustomerAuthGate({
}
}, [isPending, isAuthenticated, isCustomer, user?.role, pathname, router]);

const resolvedLoadingFallback = loadingFallback ?? (
<EstimatesQueryStatus message={loadingMessage} />
);

if (isPending || !canFetch) {
return loadingFallback ?? <EstimatesQueryStatus message={loadingMessage} />;
return resolvedLoadingFallback;
}

return children;
return (
<ProfileCompletionGuard loadingFallback={resolvedLoadingFallback}>
{children}
</ProfileCompletionGuard>
);
}
18 changes: 16 additions & 2 deletions src/components/auth/MoverAuthGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { usePathname, useRouter } from "next/navigation";
import { useEffect, type ReactNode } from "react";

import ProfileCompletionGuard from "@/components/auth/ProfileCompletionGuard";
import EstimatesQueryStatus from "@/components/estimate/EstimatesQueryStatus";
import { useMoverAuthReady } from "@/hooks/useMoverAuthReady";
import { buildLoginPath, getRoleHomePath } from "@/lib/auth/redirect";
Expand All @@ -20,6 +21,11 @@ interface MoverAuthGateProps {
* - 세션 복구 중: 로딩
* - 비로그인: 기사 로그인 페이지로 이동 (?redirect=)
* - MOVER 아님(고객·ADMIN·역할 미확정): getRoleHomePath로 이동
* - 프로필 미완료: ProfileCompletionGuard (모달)
*
* `(mover)/(protected)` layout 밖 페이지에서만 사용한다.
* layout 안에서는 RoleGuard + layout의 ProfileCompletionGuard만 쓰고,
* 이 Gate를 겹쳐 두지 않는다.
*
* // 2026.07.31 정슬기 - [추가] CustomerAuthGate와 대칭인 기사님 가드
*
Expand Down Expand Up @@ -48,11 +54,19 @@ const MoverAuthGate = ({
}
}, [isPending, isAuthenticated, isMover, user?.role, pathname, router]);

const resolvedLoadingFallback = loadingFallback ?? (
<EstimatesQueryStatus message={loadingMessage} />
);

if (isPending || !canFetch) {
return loadingFallback ?? <EstimatesQueryStatus message={loadingMessage} />;
return resolvedLoadingFallback;
}

return children;
return (
<ProfileCompletionGuard loadingFallback={resolvedLoadingFallback}>
{children}
</ProfileCompletionGuard>
);
};

export default MoverAuthGate;
57 changes: 57 additions & 0 deletions src/components/auth/ProfileCompletionGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import { usePathname } from "next/navigation";
import { type ReactNode } from "react";

import ProfileRequiredModal from "@/components/profile/ProfileRequiredModal";
import { useProfileCompletionState } from "@/hooks/profile/useProfileCompletionState";
import { isProfileCreatePath } from "@/lib/auth/redirect";
import { useAuthStore } from "@/stores/useAuthStore";

interface ProfileCompletionGuardProps {
children: ReactNode;
loadingFallback?: ReactNode;
}

/**
* 프로필 미완료 시 생성 페이지 외 보호 라우트 접근을 막습니다.
* - allowlist: /profile, /mover/profile
* - status 로딩: loadingFallback
* - 프로필 없음(404 등): incomplete → ProfileRequiredModal (닫기 불가, CTA만)
* - 그 외 status 실패: fail-open
* - 공개 페이지에는 사용하지 않음
*
* 적용 위치 (같은 트리에 Layout + AuthGate로 중복 적용하지 말 것):
* - `(protected)` Route Group → layout에서 RoleGuard와 함께
* - Group 밖 보호 페이지 → CustomerAuthGate / MoverAuthGate에서 처리
Comment thread
9g-g9 marked this conversation as resolved.
*/
const ProfileCompletionGuard = ({
children,
loadingFallback = null,
}: ProfileCompletionGuardProps) => {
const pathname = usePathname();
const role = useAuthStore((state) => state.user?.role);
const { shouldCheck, isStatusPending, isIncomplete, profileCreatePath, audience } =
useProfileCompletionState(role);

if (!shouldCheck) {
return children;
}

if (isStatusPending) {
return loadingFallback;
}

if (!isIncomplete || isProfileCreatePath(pathname, audience)) {
return children;
}

return (
<>
{loadingFallback}
<ProfileRequiredModal open profileCreatePath={profileCreatePath} />
</>
Comment on lines +49 to +53

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

차단 상태에서 배경으로 loadingFallback을 그대로 쓰고 있는데, AuthGate 경로에서는 이 값이 "견적 요청을 준비하는 중입니다." 같은 로딩 문구라 실제로는 차단인데 준비 중처럼 보일 수 있을 것 같습니다. 차단 시에는 중립 배경이나 모달만 두는 것도 좋을 것 같아요 :)

);
};

export default ProfileCompletionGuard;
Loading