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: 7 additions & 7 deletions apps/web/app/admin/banners/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useRef, useMemo, Suspense, useEffect, useLayoutEffect } from 'react';
import { useFilledSkeletonCount } from '@/lib/use-filled-skeleton-count';
import { useInitialLoadPending } from '@/lib/use-initial-load-pending';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
Expand Down Expand Up @@ -148,13 +149,12 @@ function BannerManagementPage({ embedded, onInitialContentReady }: BannerManagem

// 배너 데이터
const { data: banners = [], isLoading: bannersLoading } = useAdBannersAdmin();
const initialLoadPendingRef = useRef(true);
if (initialLoadPendingRef.current && !authLoading && !bannersLoading) initialLoadPendingRef.current = false;
const initialLoadPending = useInitialLoadPending(!authLoading && !bannersLoading);
useLayoutEffect(() => {
if (!onInitialContentReady || authLoading || initialLoadPendingRef.current) return;
if (!onInitialContentReady || authLoading || initialLoadPending) return;
onInitialContentReady();
}, [authLoading, bannersLoading, onInitialContentReady]);
const bannerListSkeleton = useFilledSkeletonCount(88, 5);
}, [authLoading, bannersLoading, initialLoadPending, onInitialContentReady]);
const { ref: bannerListRef, count: bannerListCount } = useFilledSkeletonCount(88, 5);
const createBanner = useCreateAdBanner();
const updateBanner = useUpdateAdBanner();
const deleteBanner = useDeleteAdBanner();
Expand Down Expand Up @@ -602,11 +602,11 @@ function BannerManagementPage({ embedded, onInitialContentReady }: BannerManagem
</div>
</div>

<div ref={bannerListSkeleton.ref} className="min-h-0 flex-1 space-y-2 overflow-y-auto scrollbar-hide p-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" role="list" aria-label="배너 목록">
<div ref={bannerListRef} className="min-h-0 flex-1 space-y-2 overflow-y-auto scrollbar-hide p-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" role="list" aria-label="배너 목록">
{bannersLoading ? (
<div className="space-y-2" role="status" aria-busy="true" aria-label="배너 목록 로딩 중">
<span className="sr-only">배너 목록 데이터를 불러오는 중입니다.</span>
{Array.from({ length: bannerListSkeleton.count }).map((_, index) => <BannerListItemSkeleton key={index} index={index} />)}
{Array.from({ length: bannerListCount }).map((_, index) => <BannerListItemSkeleton key={index} index={index} />)}
</div>
) : sortedBanners.length === 0 ? (
<Card className="border-dashed border-border bg-background/70 p-4 text-center text-sm text-muted-foreground">
Expand Down
29 changes: 13 additions & 16 deletions apps/web/app/admin/evaluations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useState, useEffect, useLayoutEffect, useMemo, useCallback, useRef, Suspense } from 'react';
import { useInitialLoadPending } from '@/lib/use-initial-load-pending';
import { useFilledSkeletonCount } from '@/lib/use-filled-skeleton-count';
import { useRouter, useSearchParams } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
Expand Down Expand Up @@ -1430,7 +1431,7 @@ function AdminEvaluationStaticCardSkeleton({ count }: { count: number }) {
}
function AdminEvaluationRouteSkeleton() {
const frameRef = useRef<HTMLDivElement>(null);
const mobileCards = useFilledSkeletonCount(104, 4, 48);
const { ref: mobileCardsRef, count: mobileCardsCount } = useFilledSkeletonCount(104, 4, 48);
const [rowCount, setRowCount] = useState(6);
useLayoutEffect(() => {
const node = frameRef.current;
Expand Down Expand Up @@ -1477,9 +1478,9 @@ function AdminEvaluationRouteSkeleton() {
</div>
</div>
</div>
<div ref={mobileCards.ref} className="flex min-h-0 flex-1 flex-col gap-3 p-2">
<div ref={mobileCardsRef} className="flex min-h-0 flex-1 flex-col gap-3 p-2">
<AdminEvaluationStaticMobileLoadingControls />
<AdminEvaluationStaticCardSkeleton count={mobileCards.count} />
<AdminEvaluationStaticCardSkeleton count={mobileCardsCount} />
<div ref={frameRef} className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border bg-background lg:flex">
<div className="border-b bg-muted/35 lg:grid lg:grid-cols-[40px_minmax(180px,1fr)_repeat(6,78px)_112px]" aria-hidden="true">
{Array.from({ length: 9 }).map((_, index) => (
Expand Down Expand Up @@ -3502,35 +3503,31 @@ function AdminEvaluationPage({
const initialContentLoading = initialView === 'submissions'
? submissionsLoading || recommendationRequestsLoading || reviewsLoading
: loading;
const initialLoadPendingRef = useRef(true);
if (
initialLoadPendingRef.current
&& !authLoading
&& (hasE2EAdminShellBypass || (user && isAdmin))
&& !initialContentLoading
) {
initialLoadPendingRef.current = false;
}
const initialLoadPending = useInitialLoadPending(
!authLoading
&& (hasE2EAdminShellBypass || Boolean(user && isAdmin))
&& !initialContentLoading,
);

useLayoutEffect(() => {
if (!onInitialContentReady || authLoading) return;
if (!hasE2EAdminShellBypass && (!user || !isAdmin)) {
onInitialContentReady();
return;
}
if (initialLoadPendingRef.current) return;
if (initialLoadPending) return;
onInitialContentReady();
}, [initialContentLoading, authLoading, onInitialContentReady, user, isAdmin, hasE2EAdminShellBypass]);
}, [initialContentLoading, initialLoadPending, authLoading, onInitialContentReady, user, isAdmin, hasE2EAdminShellBypass]);

if (!hasE2EAdminShellBypass && !authLoading && (!user || !isAdmin)) {
return null;
}

if (!embedded && initialLoadPendingRef.current && (authLoading || initialContentLoading)) {
if (!embedded && initialLoadPending && (authLoading || initialContentLoading)) {
return <AdminEvaluationRouteSkeleton />;
}

if (embedded && initialLoadPendingRef.current && (authLoading || initialContentLoading)) {
if (embedded && initialLoadPending && (authLoading || initialContentLoading)) {
return null;
}

Expand Down
23 changes: 13 additions & 10 deletions apps/web/app/home-runtime-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { QueryProvider } from './providers';
import { LayoutProvider } from '@/contexts/LayoutContext';
import { AnonymousHomeAuthProvider, useAuth } from '@/contexts/AuthContextBase';
import { StaticNotificationProvider } from '@/contexts/NotificationContextBase';
import { HomeSessionProviderBridge } from '@/components/home/home-session-provider-bridge';
import { useHomeViewportMode } from '@/hooks/useHomeViewportMode';
import { AUTH_UI_REQUEST_EVENT } from '@/lib/auth-ui-events';
import {
Expand Down Expand Up @@ -82,17 +83,19 @@ function HomeSessionProviders({ children }: ProviderProps) {
const AuthProvider = useDeferredComponent<ProviderProps>(hasStoredSession, loadAuthProvider);
const NotificationProvider = useDeferredComponent<ProviderProps>(hasStoredSession, loadNotificationProvider);

if (hasStoredSession && AuthProvider && NotificationProvider) {
return (
<AuthProvider>
<NotificationProvider>{children}</NotificationProvider>
</AuthProvider>
);
}

return (
<AnonymousHomeAuthProvider isLoading={isPublicRestrictedMode ? false : hasStoredSession}>
<StaticNotificationProvider>{children}</StaticNotificationProvider>
<AnonymousHomeAuthProvider
key={hasStoredSession ? 'session' : 'anonymous'}
isLoading={isPublicRestrictedMode ? false : hasStoredSession}
>
<StaticNotificationProvider>
<HomeSessionProviderBridge
AuthProvider={hasStoredSession ? AuthProvider : null}
NotificationProvider={hasStoredSession ? NotificationProvider : null}
>
{children}
</HomeSessionProviderBridge>
</StaticNotificationProvider>
</AnonymousHomeAuthProvider>
);
}
Expand Down
18 changes: 9 additions & 9 deletions apps/web/components/admin/AdminConsoleOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -713,8 +713,8 @@ function loadAdminRouteRecommendationModule() {
const ADMIN_EVALUATION_STATIC_STATUS_FILTERS = ["전체", "미처리", "승인대기", "승인됨", "누락", "삭제됨"] as const;

function AdminEvaluationModuleStaticShell() {
const mobileSkeleton = useFilledSkeletonCount(96, 4);
const desktopSkeleton = useFilledSkeletonCount(64, 6, 44);
const { ref: mobileSkeletonRef, count: mobileSkeletonCount } = useFilledSkeletonCount(96, 4);
const { ref: desktopSkeletonRef, count: desktopSkeletonCount } = useFilledSkeletonCount(64, 6, 44);
return (
<div
role="status"
Expand Down Expand Up @@ -794,8 +794,8 @@ function AdminEvaluationModuleStaticShell() {
</div>
</div>

<div ref={mobileSkeleton.ref} className="grid min-h-0 flex-1 grid-cols-1 gap-2 md:grid-cols-2 lg:hidden" aria-hidden="true">
{Array.from({ length: mobileSkeleton.count }).map((_, index) => (
<div ref={mobileSkeletonRef} className="grid min-h-0 flex-1 grid-cols-1 gap-2 md:grid-cols-2 lg:hidden" aria-hidden="true">
{Array.from({ length: mobileSkeletonCount }).map((_, index) => (
<div key={index} className="rounded-2xl border border-border/70 bg-card/95 p-3 shadow-sm">
<div className="flex items-center gap-2">
<Skeleton className="h-12 w-16 shrink-0 rounded-md motion-reduce:animate-none" />
Expand All @@ -813,7 +813,7 @@ function AdminEvaluationModuleStaticShell() {
))}
</div>

<div ref={desktopSkeleton.ref} className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border bg-background lg:flex">
<div ref={desktopSkeletonRef} className="hidden min-h-0 flex-1 flex-col overflow-hidden rounded-lg border bg-background lg:flex">
<div className="border-b bg-muted/35 lg:grid lg:grid-cols-[40px_minmax(180px,1fr)_repeat(6,78px)_112px]" aria-hidden="true">
{Array.from({ length: 9 }).map((_, index) => (
<div key={index} className="px-2 py-2">
Expand All @@ -822,7 +822,7 @@ function AdminEvaluationModuleStaticShell() {
))}
</div>
<div className="divide-y divide-border">
{Array.from({ length: desktopSkeleton.count }).map((_, rowIndex) => (
{Array.from({ length: desktopSkeletonCount }).map((_, rowIndex) => (
<div
key={rowIndex}
className="grid items-center gap-2 p-2 lg:grid-cols-[40px_minmax(180px,1fr)_repeat(6,78px)_112px]"
Expand Down Expand Up @@ -3566,16 +3566,16 @@ function AdminDashboardPanelBodySkeleton({
}: {
variant?: AdminDashboardSkeletonVariant;
}) {
const tableSkeleton = useFilledSkeletonCount(28, 5);
const { ref: tableSkeletonRef, count: tableSkeletonCount } = useFilledSkeletonCount(28, 5);
if (variant === "table") {
return (
<div
ref={tableSkeleton.ref}
ref={tableSkeletonRef}
className="min-h-0 flex-1 space-y-2 overflow-hidden rounded-xl border border-border/70 bg-background p-3"
data-admin-dashboard-dynamic-skeleton="table"
aria-hidden="true"
>
{Array.from({ length: tableSkeleton.count }).map((_, index) => (
{Array.from({ length: tableSkeletonCount }).map((_, index) => (
<div
key={index}
className="grid grid-cols-[minmax(0,1fr)_4rem_4rem] gap-3"
Expand Down
6 changes: 3 additions & 3 deletions apps/web/components/admin/AdminOverviewDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ function AdminMapOverviewCanvas({
}

function AdminMapInfoPanelSkeleton() {
const routeSkeleton = useFilledSkeletonCount(76, 4);
const { ref: routeSkeletonRef, count: routeSkeletonCount } = useFilledSkeletonCount(76, 4);
return (
<aside
className="flex min-h-0 min-w-0 flex-col gap-2 lg:h-full lg:overflow-hidden"
Expand All @@ -1058,13 +1058,13 @@ function AdminMapInfoPanelSkeleton() {
</div>
</div>
</section>
<section ref={routeSkeleton.ref} className="rounded-xl bg-card/80 p-2.5 shadow-sm lg:min-h-0 lg:flex-1">
<section ref={routeSkeletonRef} className="rounded-xl bg-card/80 p-2.5 shadow-sm lg:min-h-0 lg:flex-1">
<div className="flex items-center justify-between gap-2">
<Skeleton className="h-5 w-28 rounded-full motion-reduce:animate-none" />
<Skeleton className="h-5 w-20 rounded-full motion-reduce:animate-none" />
</div>
<div className="mt-3 space-y-1.5">
{Array.from({ length: routeSkeleton.count }).map((_, index) => (
{Array.from({ length: routeSkeletonCount }).map((_, index) => (
<Skeleton
key={index}
className="h-[4.75rem] rounded-xl motion-reduce:animate-none"
Expand Down
16 changes: 8 additions & 8 deletions apps/web/components/admin/AdminRestaurantRefreshHistoryPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useInitialLoadPending } from '@/lib/use-initial-load-pending';
import { useFilledSkeletonCount } from "@/lib/use-filled-skeleton-count";
import {
History,
Expand Down Expand Up @@ -220,17 +221,17 @@ function RefreshWorkflowSteps() {
}

function RefreshCandidateListSkeleton() {
const skeleton = useFilledSkeletonCount(92, 5);
const { ref: skeletonRef, count: skeletonCount } = useFilledSkeletonCount(92, 5);
return (
<div
ref={skeleton.ref}
ref={skeletonRef}
className="h-full min-h-0 flex-1 space-y-2 p-2 xl:divide-y xl:divide-border xl:space-y-0 xl:p-0"
role="status"
aria-busy="true"
aria-label="맛집 최신화 이력 로딩 중"
>
<span className="sr-only">맛집 최신화 후보 목록을 불러오는 중입니다.</span>
{Array.from({ length: skeleton.count }).map((_, rowIndex) => (
{Array.from({ length: skeletonCount }).map((_, rowIndex) => (
<div
key={rowIndex}
className="grid gap-3 rounded-lg border border-border/70 bg-background/80 px-3 py-3 shadow-sm xl:rounded-none xl:border-x-0 xl:border-t-0 xl:bg-transparent xl:shadow-none xl:grid-cols-[1.2fr_1fr_0.9fr_0.9fr_110px] xl:items-center"
Expand Down Expand Up @@ -636,12 +637,11 @@ export function AdminRestaurantRefreshHistoryPanel({
>("all");
const [query, setQuery] = useState("");
const [isLoading, setIsLoading] = useState(true);
const initialLoadPendingRef = useRef(true);
if (initialLoadPendingRef.current && !isLoading) initialLoadPendingRef.current = false;
const initialLoadPending = useInitialLoadPending(!isLoading);
useLayoutEffect(() => {
if (!onInitialContentReady || initialLoadPendingRef.current) return;
if (!onInitialContentReady || initialLoadPending) return;
onInitialContentReady();
}, [isLoading, onInitialContentReady]);
}, [isLoading, initialLoadPending, onInitialContentReady]);
const [error, setError] = useState<string | null>(null);
const [selectedCandidate, setSelectedCandidate] =
useState<RefreshCandidateRow | null>(null);
Expand Down
22 changes: 11 additions & 11 deletions apps/web/components/admin/AdminUsersPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useInitialLoadPending } from '@/lib/use-initial-load-pending';
import { useFilledSkeletonCount } from "@/lib/use-filled-skeleton-count";
import {
Ban,
Expand Down Expand Up @@ -133,13 +134,13 @@ function SummaryMetric({ label, value, tone = "default", isLoading = false }: {
}

function UserTableSkeleton() {
const mobileSkeleton = useFilledSkeletonCount(112, 4);
const desktopSkeleton = useFilledSkeletonCount(56, 6, 36);
const { ref: mobileSkeletonRef, count: mobileSkeletonCount } = useFilledSkeletonCount(112, 4);
const { ref: desktopSkeletonRef, count: desktopSkeletonCount } = useFilledSkeletonCount(56, 6, 36);
return (
<div ref={desktopSkeleton.ref} role="status" aria-busy="true" aria-label="사용자 목록 로딩 중" data-admin-users-loading-list className="h-full min-h-0 flex-1">
<div ref={desktopSkeletonRef} role="status" aria-busy="true" aria-label="사용자 목록 로딩 중" data-admin-users-loading-list className="h-full min-h-0 flex-1">
<span className="sr-only">사용자 목록을 불러오는 중입니다.</span>
<div ref={mobileSkeleton.ref} className="grid gap-2 md:hidden" aria-hidden="true">
{Array.from({ length: mobileSkeleton.count }).map((_, index) => (
<div ref={mobileSkeletonRef} className="grid gap-2 md:hidden" aria-hidden="true">
{Array.from({ length: mobileSkeletonCount }).map((_, index) => (
<div key={index} className="rounded-2xl border border-border/70 bg-background/80 p-3 shadow-sm">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1 space-y-1.5">
Expand Down Expand Up @@ -167,7 +168,7 @@ function UserTableSkeleton() {
</tr>
</thead>
<tbody className="divide-y divide-border/50 bg-background/70">
{Array.from({ length: desktopSkeleton.count }).map((_, index) => (
{Array.from({ length: desktopSkeletonCount }).map((_, index) => (
<tr key={index}>
<td className="min-w-0 px-3 py-3 align-top">
<button type="button" tabIndex={-1} className="block min-w-0 text-left" aria-hidden="true">
Expand Down Expand Up @@ -239,12 +240,11 @@ export default function AdminUsersPanel({
const [riskConfirmation, setRiskConfirmation] = useState("");
const [mutationResult, setMutationResult] = useState<AdminUserMutationResult | null>(null);
const [isLoading, setIsLoading] = useState(true);
const initialLoadPendingRef = useRef(true);
if (initialLoadPendingRef.current && !isLoading) initialLoadPendingRef.current = false;
const initialLoadPending = useInitialLoadPending(!isLoading);
useLayoutEffect(() => {
if (!onInitialContentReady || initialLoadPendingRef.current) return;
if (!onInitialContentReady || initialLoadPending) return;
onInitialContentReady();
}, [isLoading, onInitialContentReady]);
}, [isLoading, initialLoadPending, onInitialContentReady]);
const [isMutating, setIsMutating] = useState(false);
const [errorMessage, setErrorMessage] = useState("");

Expand Down
Loading
Loading