From 2f101c1e8590214ad9c5db728719af47ad8165c6 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:44:41 +0900 Subject: [PATCH 1/7] perf(web): retain home map across viewport changes --- apps/web/app/home-runtime-shell.tsx | 49 +- .../REPORT.md | 128 ++ .../baseline-home-runtime-shell.tsx.txt | 370 ++++ .../baseline-identity-v2/artifact-map.json | 42 + .../baseline-identity-v2/browser-bundle.mjs | 61 + .../baseline-identity-v2/build.mjs | 19 + .../baseline-identity-v2/raw.json | 1102 ++++++++++ .../baseline-identity-v2/scored.json | 17 + .../baseline-identity-v2/source-0.txt | 370 ++++ .../baseline-identity-v2/source-1.txt | 370 ++++ .../baseline-identity-v2/source-2.txt | 45 + .../baseline-identity-v2/source-3.txt | 27 + .../baseline-identity-v2/source-4.txt | 73 + .../baseline-identity-v2/source-5.txt | 108 + .../baseline-identity/rejected-build.txt | 20 + .../baseline-useHomeViewportMode.ts.txt | 45 + .../browser-entry.jsx | 75 + .../build-baseline/build.log | 1 + .../build-baseline/receipt.json | 10 + .../build-candidate/build.log | 1 + .../build-candidate/receipt.json | 9 + .../build-fixture.mjs | 57 + .../candidate-v1/artifact-map.json | 42 + .../candidate-v1/browser-bundle.mjs | 61 + .../candidate-v1/build.mjs | 19 + .../candidate-v1/raw.json | 1102 ++++++++++ .../candidate-v1/scored.json | 17 + .../candidate-v1/source-0.txt | 370 ++++ .../candidate-v1/source-1.txt | 369 ++++ .../candidate-v1/source-2.txt | 45 + .../candidate-v1/source-3.txt | 27 + .../candidate-v1/source-4.txt | 73 + .../candidate-v1/source-5.txt | 108 + .../candidate-v2/artifact-map.json | 42 + .../candidate-v2/browser-bundle.mjs | 61 + .../candidate-v2/build.mjs | 19 + .../candidate-v2/raw.json | 1102 ++++++++++ .../candidate-v2/scored.json | 17 + .../candidate-v2/source-0.txt | 370 ++++ .../candidate-v2/source-1.txt | 369 ++++ .../candidate-v2/source-2.txt | 45 + .../candidate-v2/source-3.txt | 27 + .../candidate-v2/source-4.txt | 75 + .../candidate-v2/source-5.txt | 109 + .../checks/results.json | 29 + .../checks/types.log | 1 + .../checks/unit.log | 1 + .../measure.mjs | 109 + .../playwright.config.mjs | 9 + .../route-measure.mjs | 262 +++ .../route-regression-results/.last-run.json | 4 + .../route-v3/artifact-map.json | 57 + .../route-v3/baseline-desktop.png | Bin 0 -> 86346 bytes .../route-v3/baseline-mobile.png | Bin 0 -> 34306 bytes .../route-v3/candidate-desktop.png | Bin 0 -> 86346 bytes .../route-v3/candidate-mobile.png | Bin 0 -> 34306 bytes .../route-v3/candidate-touch-detail.png | Bin 0 -> 51860 bytes .../route-v3/raw.json | 1865 +++++++++++++++++ .../route-v3/regression.log | 1 + .../route-v3/scored.json | 81 + .../route-v3/source-0.txt | 369 ++++ .../route-v3/source-1.txt | 45 + .../route-v3/source-2.txt | 17 + .../route-v3/source-3.txt | 1067 ++++++++++ .../route-v3/source-4.txt | 262 +++ .../route-v3/source-5.txt | 10 + .../route-v3/source-6.txt | 9 + .../service-stubs.jsx | 27 + .../verify-route.mjs | 64 + .../verify.mjs | 58 + .../home-root-lazy-boundary.test.ts | 1 - .../web-quality-performance-source.test.ts | 1 - .../tests/home-viewport-continuity-helpers.ts | 17 + .../tests/home-viewport-continuity.spec.ts | 27 + 74 files changed, 11834 insertions(+), 27 deletions(-) create mode 100644 apps/web/performance/home-viewport-continuity-20260930/REPORT.md create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-home-runtime-shell.tsx.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/artifact-map.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/browser-bundle.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/build.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/raw.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/scored.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-0.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-1.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-2.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-3.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-4.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity-v2/source-5.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-identity/rejected-build.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/baseline-useHomeViewportMode.ts.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/browser-entry.jsx create mode 100644 apps/web/performance/home-viewport-continuity-20260930/build-baseline/build.log create mode 100644 apps/web/performance/home-viewport-continuity-20260930/build-baseline/receipt.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/build-candidate/build.log create mode 100644 apps/web/performance/home-viewport-continuity-20260930/build-candidate/receipt.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/build-fixture.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/artifact-map.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/browser-bundle.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/build.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/raw.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/scored.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-0.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-1.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-2.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-3.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-4.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v1/source-5.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/artifact-map.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/browser-bundle.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/build.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/raw.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/scored.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-0.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-1.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-2.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-3.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-4.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/candidate-v2/source-5.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/checks/results.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/checks/types.log create mode 100644 apps/web/performance/home-viewport-continuity-20260930/checks/unit.log create mode 100644 apps/web/performance/home-viewport-continuity-20260930/measure.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/playwright.config.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-measure.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-regression-results/.last-run.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/artifact-map.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/baseline-desktop.png create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/baseline-mobile.png create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/candidate-desktop.png create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/candidate-mobile.png create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/candidate-touch-detail.png create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/raw.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/regression.log create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/scored.json create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-0.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-1.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-2.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-3.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-4.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-5.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/route-v3/source-6.txt create mode 100644 apps/web/performance/home-viewport-continuity-20260930/service-stubs.jsx create mode 100644 apps/web/performance/home-viewport-continuity-20260930/verify-route.mjs create mode 100644 apps/web/performance/home-viewport-continuity-20260930/verify.mjs create mode 100644 apps/web/tests/home-viewport-continuity-helpers.ts create mode 100644 apps/web/tests/home-viewport-continuity.spec.ts diff --git a/apps/web/app/home-runtime-shell.tsx b/apps/web/app/home-runtime-shell.tsx index de4b78f5e1..3dbf14e9cf 100644 --- a/apps/web/app/home-runtime-shell.tsx +++ b/apps/web/app/home-runtime-shell.tsx @@ -140,7 +140,7 @@ function DeferredUserDataPrefetcher({ enabled }: { enabled: boolean }) { return ; } -function MobileHomeLayout({ children }: { children: ReactNode }) { +function MobileHomeLayout() { const { user, needsNicknameSetup, completeNicknameSetup } = useAuth(); const queryClient = useQueryClient(); const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); @@ -236,21 +236,9 @@ function MobileHomeLayout({ children }: { children: ReactNode }) { return ( -
+ <> -
- - 본문 바로가기 - -
- -
- {children} -
-
-
- {!isPublicRestrictedMode && (
)} -
+ ); } function HomeLayoutContent({ children }: { children: ReactNode }) { const viewportMode = useHomeViewportMode(); - if (viewportMode === 'mobileOrTablet') { - return {children}; - } - - // 지도 본문은 pending과 desktop에서 같은 부모에 둔다. - // 오버레이 청크가 도착할 때 자식을 다른 부모로 옮기면 인기/최신 스켈레톤이 다시 마운트된다. + // 모든 뷰포트에서 지도 본문의 부모와 위치를 유지한다. + // 모바일 레이아웃을 조상으로 교체하면 지도와 검색/선택 상태가 다시 마운트된다. return ( - + : null} + > {children} {viewportMode === 'desktop' && !isPublicRestrictedMode ? ( @@ -343,16 +330,28 @@ function HomeStaticSkeleton() { ); } -function HomeRuntimePendingShell({ children }: { children: ReactNode }) { +function HomeRuntimePendingShell({ children, isMobile, mobileChrome }: { + children: ReactNode; + isMobile: boolean; + mobileChrome: ReactNode; +}) { return ( -
+
본문 바로가기 -
+
{children}
+ {mobileChrome}
); } diff --git a/apps/web/performance/home-viewport-continuity-20260930/REPORT.md b/apps/web/performance/home-viewport-continuity-20260930/REPORT.md new file mode 100644 index 0000000000..f80053d5fb --- /dev/null +++ b/apps/web/performance/home-viewport-continuity-20260930/REPORT.md @@ -0,0 +1,128 @@ +# Home viewport continuity: reproduced remount and retained measurements + +Date: 2026-09-30. Frozen baseline: `157ced98a2414d434132c99f74c72bbe5f796ca4`. + +The home shell previously replaced `HomeRuntimePendingShell` with +`MobileHomeLayout` at the mobile/tablet boundary. That change of ancestor type +discarded `HomeClient`, its map instance and its in-memory state. The candidate +keeps the map under the same main element and React child position in pending, +desktop and mobile modes. Mobile navigation/modals are siblings of that main; +their existing effects and lazy-loading boundaries remain in place. + +This fixes one reproduced remount cause. The overall rendering/flicker goal is +still active: live-provider frame/pixel evidence, authenticated interactions and +paired page metrics across the other public routes are not established here. + +## Retained results + +Each admitted timed result uses 31 paired samples, alternating variant order. +Times are medians. Delta is candidate minus baseline. Noise is twice the larger +median absolute deviation (MAD). A delta is admitted only when its absolute value +exceeds the absolute budget, 5% of the baseline and noise. + +| Measurement | Baseline | Candidate | Absolute delta | Relative delta | Noise | Absolute budget | Classification | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| Structural React lab transition, 200 memoized rows | 0.445 ms | 0.075 ms | −0.370 ms | −83.146% | 0.060 ms | 0.05 ms | Local improvement | +| Local production route: mobile nav and marker DOM ready after resize | 29.3 ms | 18.0 ms | −11.3 ms | −38.567% | 6.2 ms | 1 ms | Local improvement | +| Local production route: frames with no marker DOM | 1 frame | 0 frames | −1 frame | −100% | 0 frames | 0 frames | Local improvement | +| Local production route: maximum frame callback gap | 9.3 ms | 9.3 ms | approximately 0 ms | approximately 0% | approximately 0 ms | 1 ms | Below budget/noise | + +Structural lab row mounts/unmounts per boundary transition are **200→0**. +The complete Next route creates **1→0** new simulated provider map objects per +desktop→mobile transition. All 31 candidate route samples retained both the map +container and provider instance. Both variants had zero horizontal overflow and +one `main#main-content`. + +## What was exercised + +The structural lab compiles the actual frozen/current `HomeRuntimeShell` source +with production React. It controls the viewport mode and substitutes service +contexts and lazy chrome; the subtree has 200 memoized synthetic rows. Four +warmup pairs precede 31 scored pairs. Fourteen checks cover pending→desktop, +pending→mobile, both boundary directions, same-mode updates, and server markup +hydration followed by a controlled mode transition. The candidate preserves the +map DOM, selected synthetic value, main element and focus in every check. The +baseline loses them only on transitions that replace the mobile ancestor. + +The route experiment uses separately built Next.js 16.3.5 production bundles, +Node 24.21.0, the existing three public restaurant fixtures, and the existing +simulated Naver provider. Both variants run sequentially behind the same local +origin; the fixture helper's exact-origin check is preserved. External requests +and WebSockets are blocked. Two warmup pairs precede 31 scored pairs. + +The route readiness metric starts before the browser resize command and ends +when a frame sees the target viewport, mobile navigation, a map container and +marker DOM. It includes command transport and rendering, and is **not INP**. +Frames are then observed for a fixed one-second window. Marker absence is a DOM +predicate, not a pixel blankness detector; the simulated provider has no real map +tiles. Screenshots confirm layout geometry and the controlled detail state. + +The candidate also passed the persisted Playwright regression for mobile search +selection followed by mobile→desktop→mobile, retaining the same map container +and provider creation count. A Chromium CDP touch gesture emitted one trusted +`touchstart` and changed the selected detail from 정원분식 to 명동칼국수. This +exercises the native Chromium input/event pipeline; it does not prove physical +device/WebKit behavior or a live Naver SDK gesture. + +There were zero page runtime errors. Each sampled page in both variants recorded +two console errors under the external-request fence. Their exact cause remains +unclassified, so this experiment does not establish an error-free complete page. +Raw load/LCP counters are retained for further analysis, but no page-load gain +is admitted from them in this report. + +## Mathematical interpretation + +React retains state when component type, key and position remain stable. For R +descendant rows, replacing the ancestor triggers R cleanup/mount pairs. Keeping +that ancestor removes this O(R) remount work and preserves the provider instance. +The candidate still runs mobile chrome effects, reconciliation, device-specific +marker/layout updates and context-driven renders. Total page rendering is not +claimed to be O(1), and no live SDK initialization complexity is assumed. + +For paired timings B and C, the reported delta is median(C)−median(B), the +relative delta is 100×delta/median(B), and the noise budget is +2×max(MAD(B), MAD(C)). For the structural case, 0.370 ms exceeds 0.05 ms, +0.05×0.445=0.02225 ms, and 0.060 ms. For route readiness, 11.3 ms exceeds 1 ms, +0.05×29.3=1.465 ms, and 6.2 ms. The maximum callback-gap delta is below its 1 ms +budget and is not an improvement. + +## Provenance and verification + +Detached artifact-map pins, emitted by the measurement commands and independently +verified: + +- `candidate-v2/artifact-map.json`: `c5c6a7948a6802529643d6d86341ed0c13aa0c7a9eda9130b2a87f0427d97ac9` +- `route-v3/artifact-map.json`: `26c0f6982bd50182069ce580a6f4c4c20e6f9485ef392cf50b0b876ce81d9bc9` + +Both verifiers re-read artifact hashes and current affected source and recompute +medians, deltas, noise and admission from retained raw pairs. Build receipts bind +the two route bundles to their build IDs and shell source hashes. Frozen source, +the browser bundle, raw pairs, scored outputs, screenshots and regression output +remain in this directory. Earlier structural runs and failed route setup runs +are retained separately and are not substituted for the admitted runs. + +Both production fixture builds passed compilation, TypeScript and all 48 static +pages. Native/stable type parity passed with 2,517 logical inputs and zero +diagnostics. The initial full unit suite had 2,444 passes, one skip and one stale +source-string assertion failure. The assertion that required the old ancestor +branch was removed from both affected source-contract files; their 38 tests +then passed. The new browser regression supplies behavioral coverage for the +replaced shape. Full source lint passed after excluding the two generated local +measurement build directories; application and measurement scripts were included. +The route CSS boundary verifier also passed against the candidate build: home +192,639 raw bytes / 45,626 gzip bytes, general/admin 370,197 raw bytes / 67,656 +gzip bytes. These are validation outputs, not a CSS reduction claim. + +## Remaining goal evidence + +- Read back the live SDK and deployed revision after this source change is promoted. +- Measure actual tile/marker pixels and browser frame timing under real catalog + and provider load; include desktop/mobile and the reported flicker triggers. +- Complete authenticated-session and physical-device/WebKit interaction coverage. +- Analyze the retained load counters and extend paired measurements to the other + affected public routes before making an overall performance claim. +- Investigate the two fenced-console errors and revalidate the separate + over-capacity swipe-cache regression rather than calling all scenarios faster. + +This report is local evidence. Protected promotion, deployment and release for +this continuation were not performed at the time this report was written. diff --git a/apps/web/performance/home-viewport-continuity-20260930/baseline-home-runtime-shell.tsx.txt b/apps/web/performance/home-viewport-continuity-20260930/baseline-home-runtime-shell.tsx.txt new file mode 100644 index 0000000000..de4b78f5e1 --- /dev/null +++ b/apps/web/performance/home-viewport-continuity-20260930/baseline-home-runtime-shell.tsx.txt @@ -0,0 +1,370 @@ +'use client'; +// Keep the home Tailwind entry separate from the full app stylesheet loaded by AppRuntimeShell. + +import './home-app-globals.css'; +import { Suspense, lazy, type ComponentType, type ReactNode, useCallback, useEffect, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +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 { + AUTH_PRIVACY_ONBOARDING_REASON, + readHomeAuthLoginRequestFromLocation, +} from '@/lib/auth/auth-redirect'; +import { HOME_AUTH_SESSION_UPDATED_EVENT, type HomeAuthSessionUpdatedDetail } from '@/lib/home-auth-events'; +import { useDeferredComponent } from '@/hooks/use-deferred-component'; +import { hasSupabaseAuthSessionHint } from '@/lib/supabase-auth-session-hints'; +import { isPublicRestrictedMode } from '@/lib/site-config'; +import { + APP_HEADER_HEIGHT_VAR, + MOBILE_SHEET_HEADER_OFFSET_VAR, + MOBILE_SHEET_HEADER_PROGRESS_VAR, +} from '@/lib/mobile-sheet-layout'; + +import MobileBottomNav from '@/components/layout/MobileBottomNav'; +import { AppToaster } from '@/components/ui/app-toaster'; + +const OverlayLayout = lazy(() => import('@/components/layout/OverlayLayout')); + +type AuthModalProps = { + isOpen: boolean; + onClose: () => void; + onAuthSuccess?: () => void; + redirectTo?: string | null; + reason?: string | null; + initialAuthTab?: 'login' | 'signup'; +}; +type ProfileModalProps = AuthModalProps; +type NicknameSetupModalProps = { isOpen: boolean; onComplete: () => void }; +type ProviderProps = { children: ReactNode }; + +const loadAuthProvider = async () => { + const mod = await import('@/contexts/AuthContext'); + return mod.AuthProvider as ComponentType; +}; + +const loadNotificationProvider = async () => { + const mod = await import('@/contexts/NotificationContext'); + return mod.NotificationProvider as ComponentType; +}; + +function HomeSessionProviders({ children }: ProviderProps) { + const [hasStoredSession, setHasStoredSession] = useState( + () => !isPublicRestrictedMode && hasSupabaseAuthSessionHint(), + ); + + useEffect(() => { + if (isPublicRestrictedMode) return undefined; + + const updateSessionHint = (event?: Event) => { + const detail = (event as CustomEvent | undefined)?.detail; + if (typeof detail?.hasSession === 'boolean') { + setHasStoredSession(detail.hasSession); + return; + } + + setHasStoredSession(hasSupabaseAuthSessionHint()); + }; + + updateSessionHint(); + window.addEventListener(HOME_AUTH_SESSION_UPDATED_EVENT, updateSessionHint); + window.addEventListener('storage', updateSessionHint); + + return () => { + window.removeEventListener(HOME_AUTH_SESSION_UPDATED_EVENT, updateSessionHint); + window.removeEventListener('storage', updateSessionHint); + }; + }, []); + + const AuthProvider = useDeferredComponent(hasStoredSession, loadAuthProvider); + const NotificationProvider = useDeferredComponent(hasStoredSession, loadNotificationProvider); + + return ( + + + + {children} + + + + ); +} + +function DeferredAuthModal(props: AuthModalProps) { + const AuthModal = useDeferredComponent(props.isOpen, async () => { + const mod = await import('@/components/auth/AuthModal'); + return mod.default as ComponentType; + }); + + if (!props.isOpen || !AuthModal) return null; + return ; +} + +function DeferredProfileModal(props: ProfileModalProps) { + const ProfileModal = useDeferredComponent(props.isOpen, async () => { + const mod = await import('@/components/profile/ProfileModal'); + return mod.ProfileModal as ComponentType; + }); + + if (!props.isOpen || !ProfileModal) return null; + return ; +} + +function DeferredNicknameSetupModal(props: NicknameSetupModalProps) { + const NicknameSetupModal = useDeferredComponent(props.isOpen, async () => { + const mod = await import('@/components/profile/NicknameSetupModal'); + return mod.NicknameSetupModal as ComponentType; + }); + + if (!props.isOpen || !NicknameSetupModal) return null; + return ; +} + +function DeferredUserDataPrefetcher({ enabled }: { enabled: boolean }) { + const UserDataPrefetcher = useDeferredComponent>(enabled, async () => { + const mod = await import('@/components/layout/UserDataPrefetcher'); + return mod.default as ComponentType>; + }); + + if (!enabled || !UserDataPrefetcher) return null; + return ; +} + +function MobileHomeLayout({ children }: { children: ReactNode }) { + const { user, needsNicknameSetup, completeNicknameSetup } = useAuth(); + const queryClient = useQueryClient(); + const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); + const [authLoginRequest, setAuthLoginRequest] = useState({ + requested: false, + reason: null as string | null, + nextPath: '/', + }); + + const openAuth = useCallback(() => { + if (isPublicRestrictedMode) return; + setIsAuthModalOpen(true); + }, []); + const closeAuth = useCallback(() => { + setIsAuthModalOpen(false); + setAuthLoginRequest((current) => { + if (!current.requested) return current; + window.history.replaceState(window.history.state, '', '/'); + return { requested: false, reason: null, nextPath: '/' }; + }); + }, []); + const closeAuthAfterSuccess = useCallback(() => { + closeAuth(); + }, [closeAuth]); + + const openProfile = useCallback(() => { + if (isPublicRestrictedMode) return; + setIsProfileModalOpen(true); + }, []); + + useEffect(() => { + const root = document.documentElement; + root.style.setProperty(MOBILE_SHEET_HEADER_PROGRESS_VAR, '0'); + root.style.setProperty(MOBILE_SHEET_HEADER_OFFSET_VAR, '0px'); + root.style.setProperty(APP_HEADER_HEIGHT_VAR, '0px'); + + const openAuthListener = (event: Event) => { + const detail = (event as CustomEvent<{ route?: string }> | undefined)?.detail; + if (detail?.route && detail.route !== '/') return; + openAuth(); + }; + + const openProfileListener = (event: Event) => { + const detail = (event as CustomEvent<{ route?: string }> | undefined)?.detail; + if (detail?.route && detail.route !== '/') return; + openProfile(); + }; + + if (!isPublicRestrictedMode) { + window.addEventListener(AUTH_UI_REQUEST_EVENT, openAuth); + window.addEventListener('home:mobile-auth-request', openAuthListener); + window.addEventListener('home:mobile-profile-request', openProfileListener); + } + + return () => { + if (!isPublicRestrictedMode) { + window.removeEventListener(AUTH_UI_REQUEST_EVENT, openAuth); + window.removeEventListener('home:mobile-auth-request', openAuthListener); + window.removeEventListener('home:mobile-profile-request', openProfileListener); + } + root.style.setProperty(MOBILE_SHEET_HEADER_PROGRESS_VAR, '0'); + root.style.setProperty(MOBILE_SHEET_HEADER_OFFSET_VAR, '0px'); + root.style.setProperty(APP_HEADER_HEIGHT_VAR, '56px'); + }; + }, [openAuth, openProfile]); + + useEffect(() => { + const request = readHomeAuthLoginRequestFromLocation(window.location); + if (request.requested && isPublicRestrictedMode) { + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.delete('auth'); + currentUrl.searchParams.delete('reason'); + currentUrl.searchParams.delete('next'); + const nextSearch = currentUrl.searchParams.toString(); + const nextUrl = `${currentUrl.pathname}${nextSearch ? `?${nextSearch}` : ''}${currentUrl.hash}`; + window.history.replaceState(window.history.state, '', nextUrl); + setAuthLoginRequest({ requested: false, reason: null, nextPath: '/' }); + return; + } + + setAuthLoginRequest(request); + if (request.requested && !isPublicRestrictedMode) { + setIsAuthModalOpen(true); + } + }, []); + + useEffect(() => { + if (!user) { + queryClient.removeQueries({ queryKey: ['user-bookmarks'] }); + } + }, [queryClient, user]); + + + return ( +
+ + +
+ + 본문 바로가기 + +
+ +
+ {children} +
+
+
+ + {!isPublicRestrictedMode && ( +
+ +
+ )} + + {!isPublicRestrictedMode && isAuthModalOpen && ( + + + + )} + + {!isPublicRestrictedMode && isProfileModalOpen && ( + + setIsProfileModalOpen(false)} + /> + + )} + + {!isPublicRestrictedMode && needsNicknameSetup && ( + + + + )} +
+ ); +} + +function HomeLayoutContent({ children }: { children: ReactNode }) { + const viewportMode = useHomeViewportMode(); + + if (viewportMode === 'mobileOrTablet') { + return {children}; + } + + // 지도 본문은 pending과 desktop에서 같은 부모에 둔다. + // 오버레이 청크가 도착할 때 자식을 다른 부모로 옮기면 인기/최신 스켈레톤이 다시 마운트된다. + return ( + + {children} + {viewportMode === 'desktop' && !isPublicRestrictedMode ? ( + + + + ) : null} + + ); +} + +function HomeStaticSkeleton() { + return ( +