From 2f78cacd456cc3a286f34111cfc8629562569271 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:31:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=98=20=EB=B0=98=EB=B3=B5=20=ED=8E=98=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=EC=99=80=20=EB=A7=88=EC=BB=A4=20=ED=95=A0=EB=8B=B9=EC=9D=84=20?= =?UTF-8?q?=EC=A4=84=EC=9D=B4=EA=B3=A0,=20=EC=8A=A4=ED=83=AC=ED=94=84=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=EB=AC=B8=EC=9E=A5?= =?UTF-8?q?=EA=B3=BC=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- README.md | 54 +- apps/web/.env.example | 12 - apps/web/app/admin/banners/page.tsx | 24 +- apps/web/app/admin/evaluations/page.tsx | 71 +- .../api/privacy/location-readiness/route.ts | 6 +- apps/web/app/app-globals.css | 48 +- .../fonts/pretendard/PretendardVariable.woff2 | Bin 2057688 -> 297492 bytes apps/web/app/global-map/page.tsx | 6 +- apps/web/app/home-app-globals.css | 70 +- apps/web/app/home-client.tsx | 9 +- apps/web/app/home-deferred-globals.css | 45 +- apps/web/app/home-detail-globals.css | 45 +- apps/web/app/home-runtime-shell.tsx | 7 +- apps/web/app/mypage/profile/page.tsx | 8 +- apps/web/app/mypage/reviews/page.tsx | 2 +- .../components/admin/AdminConsoleOverview.tsx | 234 ++---- .../admin/AdminOverviewDashboard.tsx | 6 +- .../components/admin/AdminRestaurantModal.tsx | 2 +- .../AdminRestaurantRefreshHistoryPanel.tsx | 21 +- apps/web/components/admin/AdminUsersPanel.tsx | 25 +- .../components/admin/EvaluationTableNew.tsx | 24 +- .../components/admin/SubmissionListView.tsx | 6 +- .../announcement/AnnouncementPanel.tsx | 2 +- .../AnnouncementPanelLoadingFallback.tsx | 2 +- apps/web/components/feed/FeedContent.tsx | 11 +- .../home/DesktopLeftPanelBookmarks.tsx | 45 +- .../home/DesktopLeftPanelMapHome.tsx | 11 +- .../home/DesktopLeftPanelNotifications.tsx | 70 +- apps/web/components/home/HomeMapUserMenu.tsx | 10 +- .../components/home/MobileControlOverlay.tsx | 36 +- .../home/SubmissionFloatingButton.tsx | 3 - .../home/home-desktop-control-panel.tsx | 64 +- .../components/home/home-map-container.tsx | 77 +- apps/web/components/home/map-panel-chrome.tsx | 60 ++ apps/web/components/layout/CombinedPopup.tsx | 39 +- .../components/layout/FloatingNavButtons.tsx | 8 +- apps/web/components/layout/Header.tsx | 26 +- .../layout/HeaderBookmarkMenuButton.tsx | 2 +- apps/web/components/layout/MainLayout.tsx | 8 +- .../web/components/layout/MobileBottomNav.tsx | 7 +- .../components/layout/OverlayPagePanel.tsx | 12 +- apps/web/components/layout/RankingWidget.tsx | 2 +- .../components/layout/ReturnToMapButton.tsx | 2 +- apps/web/components/map/MapView.tsx | 2 +- apps/web/components/map/NaverMapView.tsx | 732 ++++++++++++------ apps/web/components/map/OverseasMap.tsx | 4 +- .../web/components/map/map-overlay-notice.tsx | 2 +- .../map/map-view-overlay-indicators.tsx | 2 +- .../components/map/map-view-sidepanels.tsx | 10 +- .../components/map/naver-map-sidepanels.tsx | 12 +- .../components/modals/EditRestaurantModal.tsx | 2 +- .../modals/RestaurantSubmissionModal.tsx | 2 +- apps/web/components/mypage/MyPageSidebar.tsx | 14 +- .../components/mypage/MyPageTopActions.tsx | 2 +- .../overlay-pages/LeaderboardOverlay.tsx | 43 +- .../components/overlay-pages/StampOverlay.tsx | 49 +- .../components/profile/UserProfilePanel.tsx | 10 +- .../UserProfileProgressiveSkeleton.tsx | 7 +- .../DailyRecommendationPopup.tsx | 14 +- .../restaurant/RestaurantDetailPanel.tsx | 211 +++-- apps/web/components/reviews/ReviewCard.tsx | 8 +- apps/web/components/reviews/ReviewModal.tsx | 31 +- .../stamp/RestaurantReviewsPanel.tsx | 2 +- apps/web/components/stamp/StampCard.tsx | 29 +- apps/web/components/ui/alert-dialog.tsx | 4 +- apps/web/components/ui/badge.tsx | 2 +- apps/web/components/ui/bookmark-button.tsx | 2 +- apps/web/components/ui/bottom-sheet.tsx | 73 +- apps/web/components/ui/button.tsx | 2 +- apps/web/components/ui/carousel.tsx | 8 +- apps/web/components/ui/dialog.tsx | 4 +- apps/web/components/ui/dropdown-menu.tsx | 10 +- apps/web/components/ui/mobile-sheet-frame.tsx | 6 +- apps/web/components/ui/popover.tsx | 2 +- apps/web/components/ui/select.tsx | 2 +- apps/web/components/ui/sheet.tsx | 14 +- apps/web/components/ui/skeleton-loaders.tsx | 11 +- apps/web/components/ui/tabs.tsx | 2 +- apps/web/components/ui/tooltip.tsx | 2 +- apps/web/components/ui/youtube-thumbnail.tsx | 92 ++- apps/web/hooks/use-restaurants.tsx | 4 +- apps/web/hooks/useMapOptimization.ts | 4 +- apps/web/lib/admin/admin-module-routing.ts | 1 - apps/web/lib/admin/sidebar-order.ts | 3 +- apps/web/lib/cluster-marker.ts | 113 +-- apps/web/lib/clustering.ts | 20 +- apps/web/lib/debug-log.ts | 12 +- apps/web/lib/home-map-mode-filter.ts | 68 +- apps/web/lib/home-map-swipe-restaurants.ts | 52 +- apps/web/lib/home-map-youtube-kpi.ts | 20 + apps/web/lib/html-escape.ts | 2 +- apps/web/lib/map-render-guard.ts | 91 ++- apps/web/lib/map-view-helpers.ts | 36 +- apps/web/lib/map-view-marker-helpers.ts | 6 +- apps/web/lib/marker-pool.ts | 89 ++- apps/web/lib/mobile-home-search-selection.ts | 129 +-- apps/web/lib/mobile-sheet-layout.ts | 6 +- apps/web/lib/naver-map-cluster-visuals.ts | 86 +- apps/web/lib/naver-map-marker-visuals.ts | 114 ++- apps/web/lib/naver-map-overlap-helpers.ts | 40 + apps/web/lib/naver-map-overlay-timings.ts | 6 +- apps/web/lib/naver-map-render-plan.ts | 230 +++++- apps/web/lib/naver-map-view-helpers.ts | 36 +- apps/web/lib/naver-map-wheel-helpers.ts | 8 + apps/web/lib/overseas-map-helpers.ts | 34 +- apps/web/lib/overseas-map-marker-helpers.ts | 2 +- apps/web/lib/popular-restaurant-score.ts | 136 ++++ apps/web/lib/popular-restaurants.ts | 286 ++++--- apps/web/lib/privacy/location-gate.ts | 5 +- apps/web/lib/privacy/location-readiness.ts | 42 +- apps/web/lib/restaurant-merged-media.ts | 74 +- apps/web/lib/review-photo-url.ts | 7 +- apps/web/lib/security/same-origin-mutation.ts | 5 + apps/web/lib/use-filled-skeleton-count.ts | 46 ++ apps/web/lib/visible-marker-review-bubbles.ts | 122 ++- apps/web/lib/youtube-thumbnail.ts | 38 +- .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../bottom-nav-scale-20260924/benchmark.json | 9 + .../benchmark.json | 12 + .../benchmark.json | 8 + .../benchmark.json | 9 + .../benchmark-cluster-animation-scheduler.mjs | 119 +++ .../benchmark.json | 43 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark-cluster-category-cache.mjs | 92 +++ .../benchmark.json | 20 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 12 + .../benchmark.json | 10 + .../benchmark.json | 12 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 11 + .../benchmark.json | 7 + .../benchmark.json | 28 + .../benchmark.json | 7 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 30 + .../benchmark.json | 10 + .../benchmark.json | 15 + .../benchmark.json | 10 + .../benchmark.json | 22 + .../benchmark.json | 15 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 19 + .../dialog-enter-snap-20260924/benchmark.json | 10 + .../benchmark.json | 9 + .../global-map-blur-20260924/benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../header-color-snap-20260924/benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../home-client-blur-20260924/benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 16 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../logo-resize-20260924/benchmark.json | 7 + .../main-margin-snap-20260924/benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 22 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 15 + .../benchmark.json | 11 + .../map-shadow-paint-20260924/benchmark.json | 11 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../marker-box-shadow-20260924/benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 24 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 9 + .../benchmark.json | 8 + .../benchmark.json | 10 + .../benchmark.json | 11 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../benchmark.json | 11 + .../benchmark-marker-visual-cache.mjs | 93 +++ .../benchmark.json | 31 + .../benchmark.json | 9 + .../menu-enter-snap-20260924/benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 8 + .../popup-arrow-fade-20260924/benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../pretendard-subset-20260924/benchmark.json | 26 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 15 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 13 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 15 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 11 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 12 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 11 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 11 + .../sheet-enter-snap-20260924/benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 15 + .../benchmark.json | 10 + .../benchmark.json | 21 + .../benchmark.json | 9 + .../benchmark.json | 10 + .../benchmark.json | 13 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../ui-noise-tile-20260924/benchmark.json | 7 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 11 + .../benchmark.json | 11 + .../benchmark.json | 11 + .../benchmark.json | 10 + .../benchmark.json | 9 + .../benchmark.json | 9 + .../benchmark.json | 20 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 10 + .../benchmark.json | 38 + .../benchmark.json | 9 + apps/web/public/images/maker-images/asian.png | Bin 103653 -> 27047 bytes .../images/maker-images/cafe_dessert.png | Bin 92029 -> 24172 bytes .../public/images/maker-images/chicken.png | Bin 89040 -> 23851 bytes .../public/images/maker-images/chinese.png | Bin 112048 -> 29407 bytes .../images/maker-images/cutlet_sashimi.png | Bin 92861 -> 23823 bytes .../public/images/maker-images/fastfood.png | Bin 128428 -> 33714 bytes .../web/public/images/maker-images/korean.png | Bin 123795 -> 31870 bytes .../public/images/maker-images/late_night.png | Bin 103474 -> 27434 bytes .../public/images/maker-images/lunch_box.png | Bin 116113 -> 30560 bytes .../public/images/maker-images/meat_bbq.png | Bin 97921 -> 25099 bytes apps/web/public/images/maker-images/pizza.png | Bin 87729 -> 23847 bytes .../public/images/maker-images/pork_feet.png | Bin 109938 -> 28532 bytes .../public/images/maker-images/seasonal.png | Bin 549570 -> 16192 bytes .../public/images/maker-images/snack_bar.png | Bin 104434 -> 27563 bytes apps/web/public/images/maker-images/stew.png | Bin 94619 -> 24539 bytes apps/web/public/images/maker-images/trend.png | Bin 482446 -> 12418 bytes .../images/maker-images/user_submitted.png | Bin 440277 -> 12856 bytes .../images/maker-images/webp/seasonal.webp | Bin 63006 -> 3232 bytes .../images/maker-images/webp/trend.webp | Bin 50206 -> 2718 bytes .../maker-images/webp/user_submitted.webp | Bin 46756 -> 2698 bytes .../public/images/maker-images/western.png | Bin 104122 -> 26909 bytes apps/web/public/images/stamp-clear-dark.png | Bin 913040 -> 338795 bytes apps/web/public/images/stamp-clear-dark.webp | Bin 0 -> 74392 bytes apps/web/public/images/stamp-clear.png | Bin 934794 -> 343803 bytes apps/web/public/images/stamp-clear.webp | Bin 0 -> 82216 bytes apps/web/public/images/ui-noise.png | Bin 0 -> 3466 bytes apps/web/public/logo.webp | Bin 56644 -> 5578 bytes .../admin-console-uiux-source.test.ts | 23 +- .../admin-evaluation-responsive-css.test.ts | 6 +- ...n-evaluation-status-filter-options.test.ts | 2 +- .../admin-map-overlays-ia-contract.test.ts | 30 +- .../tests-unit/admin-sidebar-order.test.ts | 5 +- .../device-location-privacy.test.ts | 2 +- ...rontend-unused-route-compatibility.test.ts | 2 +- ...er-bookmark-notification-ux-source.test.ts | 21 +- ...-map-contextual-restaurants-source.test.ts | 2 +- .../tests-unit/home-map-mode-filter.test.ts | 15 +- .../home-map-swipe-restaurants.test.ts | 7 +- .../map-view-marker-helpers.test.ts | 4 +- apps/web/tests-unit/marker-pool.test.ts | 126 ++- .../mobile-desktop-parity-source.test.ts | 2 +- .../mobile-home-map-regressions.test.ts | 3 +- .../mobile-home-search-selection.test.ts | 26 +- .../tests-unit/mobile-sheet-layout.test.ts | 12 + .../mypage-mobile-cleanup-contract.test.ts | 9 +- .../naver-map-cluster-visuals.test.ts | 51 ++ .../naver-map-marker-visuals.test.ts | 8 + .../naver-map-overlap-helpers.test.ts | 34 + .../naver-map-overlay-stack.test.tsx | 2 +- .../tests-unit/naver-map-render-guard.test.ts | 22 +- .../tests-unit/naver-map-render-plan.test.ts | 98 +++ .../naver-map-toast-helpers.test.ts | 16 +- .../naver-map-wheel-helpers.test.ts | 7 + .../overseas-map-marker-helpers.test.ts | 2 +- .../popular-restaurant-score.test.ts | 66 ++ apps/web/tests-unit/privacy-sanitize.test.ts | 3 +- .../restaurant-merged-media.test.ts | 46 ++ .../restaurant-visit-count-source.test.ts | 3 +- .../tests-unit/same-origin-mutation.test.ts | 17 + .../visible-marker-review-bubbles.test.ts | 19 + .../web-quality-performance-source.test.ts | 59 +- .../tests-unit/youtube-link-helpers.test.ts | 8 + .../admin-console-module-hydration.spec.ts | 5 - 378 files changed, 5818 insertions(+), 2030 deletions(-) create mode 100644 apps/web/components/home/map-panel-chrome.tsx create mode 100644 apps/web/lib/popular-restaurant-score.ts create mode 100644 apps/web/lib/use-filled-skeleton-count.ts create mode 100644 apps/web/performance/announcement-count-pulse-20260924/benchmark.json create mode 100644 apps/web/performance/bottom-nav-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/bottom-nav-hide-snap-20260924/benchmark.json create mode 100644 apps/web/performance/bottom-nav-scale-20260924/benchmark.json create mode 100644 apps/web/performance/category-icon-webp-20260924/benchmark.json create mode 100644 apps/web/performance/category-marker-png-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-animation-listener-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-animation-scheduler-20260924/benchmark-cluster-animation-scheduler.mjs create mode 100644 apps/web/performance/cluster-animation-scheduler-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-badge-backdrop-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-badge-shadow-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-category-cache-20260924/benchmark-cluster-category-cache.mjs create mode 100644 apps/web/performance/cluster-category-cache-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-category-join-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-category-rotation-stop-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-fade-flicker-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-html-cache-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-icon-dom-swap-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-icon-png-assign-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-icon-preload-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-leaf-read-skip-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-plan-object-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-point-properties-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-register-skip-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-still-icon-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-visual-key-hash-20260924/benchmark.json create mode 100644 apps/web/performance/cluster-visual-object-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/compact-map-duplicate-name-20260924/benchmark.json create mode 100644 apps/web/performance/compact-map-review-text-20260924/benchmark.json create mode 100644 apps/web/performance/compact-map-unused-columns-20260924/benchmark.json create mode 100644 apps/web/performance/contextual-id-signature-20260924/benchmark.json create mode 100644 apps/web/performance/coordinate-pass-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/daily-recommendation-fade-20260924/benchmark.json create mode 100644 apps/web/performance/daily-thumbnail-hover-filter-20260924/benchmark.json create mode 100644 apps/web/performance/debug-log-sanitizer-split-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-map-backdrop-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-map-menu-split-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-panel-blur-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-panel-detail-split-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-panel-rest-list-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-panel-slide-20260924/benchmark.json create mode 100644 apps/web/performance/desktop-panel-will-change-20260924/benchmark.json create mode 100644 apps/web/performance/detail-panel-width-snap-20260924/benchmark.json create mode 100644 apps/web/performance/detail-play-carousel-snap-20260924/benchmark.json create mode 100644 apps/web/performance/detail-review-lookup-gate-20260924/benchmark.json create mode 100644 apps/web/performance/dialog-enter-snap-20260924/benchmark.json create mode 100644 apps/web/performance/extended-bounds-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/global-map-blur-20260924/benchmark.json create mode 100644 apps/web/performance/google-map-selection-center-20260924/benchmark.json create mode 100644 apps/web/performance/google-marker-bounce-20260924/benchmark.json create mode 100644 apps/web/performance/header-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/header-offset-snap-20260924/benchmark.json create mode 100644 apps/web/performance/header-texture-fade-20260924/benchmark.json create mode 100644 apps/web/performance/header-transition-snap-20260924/benchmark.json create mode 100644 apps/web/performance/home-banner-popup-fade-20260924/benchmark.json create mode 100644 apps/web/performance/home-bottom-nav-snap-20260924/benchmark.json create mode 100644 apps/web/performance/home-client-blur-20260924/benchmark.json create mode 100644 apps/web/performance/home-map-control-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/home-skeleton-phase-20260924/benchmark.json create mode 100644 apps/web/performance/hover-opacity-snap-20260924/benchmark.json create mode 100644 apps/web/performance/image-filter-hover-20260924/benchmark.json create mode 100644 apps/web/performance/individual-marker-visual-identity-20260924/benchmark.json create mode 100644 apps/web/performance/list-card-direction-sheet-20260924/benchmark.json create mode 100644 apps/web/performance/logo-resize-20260924/benchmark.json create mode 100644 apps/web/performance/main-margin-snap-20260924/benchmark.json create mode 100644 apps/web/performance/map-chrome-backdrop-20260924/benchmark.json create mode 100644 apps/web/performance/map-chunk-public-url-20260924/benchmark.json create mode 100644 apps/web/performance/map-control-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/map-count-badge-fade-20260924/benchmark.json create mode 100644 apps/web/performance/map-detail-skeleton-pulse-20260924/benchmark.json create mode 100644 apps/web/performance/map-list-publish-skip-20260924/benchmark.json create mode 100644 apps/web/performance/map-mode-button-transition-20260924/benchmark.json create mode 100644 apps/web/performance/map-mode-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/map-mode-filter-identity-20260924/benchmark.json create mode 100644 apps/web/performance/map-mode-filter-rescan-20260924/benchmark.json create mode 100644 apps/web/performance/map-overlay-fade-in-20260924/benchmark.json create mode 100644 apps/web/performance/map-overlay-fade-out-20260924/benchmark.json create mode 100644 apps/web/performance/map-shadow-paint-20260924/benchmark.json create mode 100644 apps/web/performance/map-submit-button-transition-20260924/benchmark.json create mode 100644 apps/web/performance/map-user-button-snap-20260924/benchmark.json create mode 100644 apps/web/performance/map-width-transition-20260924/benchmark.json create mode 100644 apps/web/performance/marker-anchor-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/marker-bounce-stop-20260924/benchmark.json create mode 100644 apps/web/performance/marker-bounds-signature-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/marker-box-shadow-20260924/benchmark.json create mode 100644 apps/web/performance/marker-fade-out-remove-20260924/benchmark.json create mode 100644 apps/web/performance/marker-filter-layer-20260924/benchmark.json create mode 100644 apps/web/performance/marker-image-decode-20260924/benchmark.json create mode 100644 apps/web/performance/marker-image-resize-20260924/benchmark.json create mode 100644 apps/web/performance/marker-kind-signature-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/marker-latlng-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/marker-pool-reclaim-20260924/benchmark.json create mode 100644 apps/web/performance/marker-render-early-return-20260924/benchmark.json create mode 100644 apps/web/performance/marker-shadow-pass-20260924/benchmark.json create mode 100644 apps/web/performance/marker-signature-id-sort-20260924/benchmark.json create mode 100644 apps/web/performance/marker-signature-strings-20260924/benchmark.json create mode 100644 apps/web/performance/marker-skip-dom-query-20260924/benchmark.json create mode 100644 apps/web/performance/marker-skip-key-allocation-20260924/benchmark.json create mode 100644 apps/web/performance/marker-transform-transition-20260924/benchmark.json create mode 100644 apps/web/performance/marker-transition-all-20260924/benchmark.json create mode 100644 apps/web/performance/marker-viewport-padding-20260924/benchmark.json create mode 100644 apps/web/performance/marker-visual-cache-20260924/benchmark-marker-visual-cache.mjs create mode 100644 apps/web/performance/marker-visual-cache-20260924/benchmark.json create mode 100644 apps/web/performance/menu-badge-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/menu-enter-snap-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-map-backdrop-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-overlay-poll-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-search-fade-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-search-slide-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-sheet-height-snap-20260924/benchmark.json create mode 100644 apps/web/performance/mobile-sheet-sticky-blur-20260924/benchmark.json create mode 100644 apps/web/performance/mypage-sidebar-ring-snap-20260924/benchmark.json create mode 100644 apps/web/performance/nearest-distance-square-20260924/benchmark.json create mode 100644 apps/web/performance/nearest-fallback-cache-20260924/benchmark.json create mode 100644 apps/web/performance/nearest-fallback-distance-20260924/benchmark.json create mode 100644 apps/web/performance/overlap-offset-memo-20260924/benchmark.json create mode 100644 apps/web/performance/overlay-panel-scrim-20260924/benchmark.json create mode 100644 apps/web/performance/overlay-panel-width-snap-20260924/benchmark.json create mode 100644 apps/web/performance/overseas-location-jump-20260924/benchmark.json create mode 100644 apps/web/performance/overseas-marker-layer-20260924/benchmark.json create mode 100644 apps/web/performance/panel-offset-pan-snap-20260924/benchmark.json create mode 100644 apps/web/performance/popular-list-reasoning-20260924/benchmark.json create mode 100644 apps/web/performance/popup-arrow-fade-20260924/benchmark.json create mode 100644 apps/web/performance/popup-banner-slide-snap-20260924/benchmark.json create mode 100644 apps/web/performance/popup-dot-transition-20260924/benchmark.json create mode 100644 apps/web/performance/pretendard-subset-20260924/benchmark.json create mode 100644 apps/web/performance/profile-avatar-ring-snap-20260924/benchmark.json create mode 100644 apps/web/performance/profile-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/profile-tier-bar-snap-20260924/benchmark.json create mode 100644 apps/web/performance/ranking-widget-fade-20260924/benchmark.json create mode 100644 apps/web/performance/render-target-id-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/render-target-key-allocation-20260924/benchmark.json create mode 100644 apps/web/performance/renderable-coordinate-identity-20260924/benchmark.json create mode 100644 apps/web/performance/restaurant-identity-allocation-20260924/benchmark.json create mode 100644 apps/web/performance/return-bookmark-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-candidate-lists-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-candidate-object-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-clamp-layout-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-clamp-skip-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-html-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-id-signature-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-key-array-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-key-join-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-latlng-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-layout-read-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-list-lazy-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-marker-patch-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-projection-cache-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-rank-string-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-related-ids-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-render-token-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-score-skip-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-seed-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/review-bubble-target-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/review-card-transition-20260924/benchmark.json create mode 100644 apps/web/performance/review-carousel-blur-20260924/benchmark.json create mode 100644 apps/web/performance/review-form-shadow-snap-20260924/benchmark.json create mode 100644 apps/web/performance/review-heart-transition-20260924/benchmark.json create mode 100644 apps/web/performance/review-list-skeleton-pulse-20260924/benchmark.json create mode 100644 apps/web/performance/review-ocr-highlight-20260924/benchmark.json create mode 100644 apps/web/performance/review-pin-dot-transition-20260924/benchmark.json create mode 100644 apps/web/performance/selected-marker-loop-20260924/benchmark.json create mode 100644 apps/web/performance/selected-pulse-overseas-shadow-20260924/benchmark.json create mode 100644 apps/web/performance/selection-camera-snap-20260924/benchmark.json create mode 100644 apps/web/performance/shared-bottom-sheet-snap-20260924/benchmark.json create mode 100644 apps/web/performance/shared-button-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/shared-sheet-snap-state-20260924/benchmark.json create mode 100644 apps/web/performance/sheet-enter-snap-20260924/benchmark.json create mode 100644 apps/web/performance/sheet-snap-transition-state-20260924/benchmark.json create mode 100644 apps/web/performance/sheet-will-change-height-20260924/benchmark.json create mode 100644 apps/web/performance/sidebar-recommendation-color-snap-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-card-content-visibility-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-card-hover-shadow-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-card-thumbnail-ceiling-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-card-transition-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-overlay-resize-20260924/benchmark.json create mode 100644 apps/web/performance/stamp-thumbnail-opacity-fade-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-dedupe-identity-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-dedupe-index-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-dedupe-rescan-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-hint-backdrop-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-list-identity-20260924/benchmark.json create mode 100644 apps/web/performance/swipe-order-single-pass-20260924/benchmark.json create mode 100644 apps/web/performance/tab-transition-snap-20260924/benchmark.json create mode 100644 apps/web/performance/ui-noise-tile-20260924/benchmark.json create mode 100644 apps/web/performance/unused-map-keyframes-20260924/benchmark.json create mode 100644 apps/web/performance/unused-marker-keyframes-20260924/benchmark.json create mode 100644 apps/web/performance/viewport-cell-index-20260924/benchmark.json create mode 100644 apps/web/performance/viewport-cell-inline-20260924/benchmark.json create mode 100644 apps/web/performance/viewport-cell-key-number-20260924/benchmark.json create mode 100644 apps/web/performance/visible-id-set-identity-20260924/benchmark.json create mode 100644 apps/web/performance/visible-id-set-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/visible-list-copy-skip-20260924/benchmark.json create mode 100644 apps/web/performance/visible-list-reuse-20260924/benchmark.json create mode 100644 apps/web/performance/visible-list-single-scan-20260924/benchmark.json create mode 100644 apps/web/performance/visit-badge-shadow-20260924/benchmark.json create mode 100644 apps/web/performance/wheel-zoom-same-turn-20260924/benchmark.json create mode 100644 apps/web/performance/youtube-thumbnail-ceiling-20260924/benchmark.json create mode 100644 apps/web/performance/youtube-thumbnail-hold-20260924/benchmark.json create mode 100644 apps/web/public/images/stamp-clear-dark.webp create mode 100644 apps/web/public/images/stamp-clear.webp create mode 100644 apps/web/public/images/ui-noise.png create mode 100644 apps/web/tests-unit/mobile-sheet-layout.test.ts create mode 100644 apps/web/tests-unit/popular-restaurant-score.test.ts diff --git a/README.md b/README.md index a4ad3b6bb7..fc28cecca4 100644 --- a/README.md +++ b/README.md @@ -73,45 +73,31 @@ Tzudong Map turns mukbang video evidence into a usable restaurant map: users dis -## Local architecture +## Architecture -System map: [interactive system architecture](docs/architecture/tzudong-system-architecture.html) · [Archify source](docs/architecture/tzudong-system-architecture.archify.json) +Public map, admin, and the evidence pipeline meet in Supabase. Long work stays off the web request. -Worker admission flow: [interactive worker boosting flow](docs/architecture/tzudong-worker-boosting.workflow.html) · [Archify source](docs/architecture/tzudong-worker-boosting.workflow.archify.json) +```mermaid +flowchart LR + web["apps/web"] --> db[("Supabase")] + ops["CI / cron"] --> control["pipeline-api"] + control --> worker["pipeline-worker"] + worker --> crawl["crawler / evaluation"] + crawl --> db + crawl --> ext["External providers"] + worker --> db +``` -Interactive workflow: [storyboard-local-mlx.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.html) +Open a diagram to pan, zoom, and follow a path. Sources sit next to each HTML file. -Workflow source: [storyboard-local-mlx.workflow.json](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.workflow.json) - -Interactive lifecycle: [storyboard-local-mlx.lifecycle.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.lifecycle.html) - -Lifecycle source: [storyboard-local-mlx.lifecycle.json](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.lifecycle.json) - -Interactive map discovery: [map-discovery.html](docs/architecture/map-discovery/map-discovery.html) - -Live browser captures: [workflow](docs/operations/evidence/storyboard-restore-20260920/archify-storyboard-workflow-desktop.png) · [lifecycle](docs/operations/evidence/storyboard-restore-20260920/archify-storyboard-lifecycle-desktop.png) · [map discovery](docs/operations/evidence/storyboard-restore-20260920/archify-map-discovery-desktop.png) - -Map discovery source: [map-discovery.workflow.json](docs/architecture/map-discovery/map-discovery.workflow.json) - -Evidence pipeline: [interactive data pipeline](docs/architecture/data-pipeline/tzudong-data-pipeline.html) · [dataflow source](docs/architecture/data-pipeline/tzudong-data-pipeline.dataflow.json) - -Worker flow: [interactive worker boosting flow](docs/architecture/data-pipeline/worker-boosting.html) · [workflow source](docs/architecture/data-pipeline/worker-boosting.workflow.json) - -The worker keeps image concurrency at `c=1`. A future bounded boost must pass memory admission and observed queue/run/RSS evidence before changing that cap; dynamic concurrency increase is not claimed as implemented. Queue claim and lease ownership, heartbeat freshness, revision checks, conditional save/readback, and late-write rejection define the persistence boundary. Operator observations are bounded and non-authoritative, with secrets and raw payloads excluded. - -The local workspace separates **new project creation**, **scene editing**, **version history**, and **manual result import**. Saved projects open directly on the scene editor; connection diagnostics are under Settings. Text and image providers remain independent: local MLX, ChatGPT manual import, and Grok manual import are selectable with external AI off. OpenAI/xAI official APIs stay gated and currently unconfigured. There is no automatic cloud fallback. - -Storyboard provider policy keeps `local-mlx`, `chatgpt-manual`, and `grok-manual` independently selectable for text and image without requiring an official external API; the manual choices return through user import. The `externalAI` consent gate applies only to the official API provider IDs `openai-api` and `xai-api`. - -Historical restoration now uses immutable DB snapshots. Version preview and a confirmed whole-project or single-scene restore preserve the historical text, scene order, asset references, provenance and original image bytes. Restoration writes a new revision without calling a model or queueing a job; current provider settings and consent are retained. Applied projects start history from the state captured by the migration, not from invented earlier versions. Owner/admin checks, current revision, busy-project rejection and idempotent request IDs protect the operation. - -The memory admission model is implemented in `apps/web/lib/admin/storyboard/resource-invariants.ts`: `used + additional_peak_estimate + reserve <= physical`, with initial reserve `max(16 GiB, 12.5% of physical RAM)`. The worker now uses host-wide `os.freemem()` by default and only adds model residency in the explicit RSS fallback; measurements and peak estimates are not absolute guarantees. The [runtime audit on 2026-09-21](docs/operations/evidence/storyboard-restore-20260920/runtime-audit-20260921.md) records the remaining stress-test and peak-inference limits. The queue-wait helper is currently unit-tested but has no runtime caller. Image concurrency starts at one. Queue wait follows `W[i+1] = max(0, W[i] + S[i] - A[i])`; absent a live worker, estimated wait is unknown. Atomic claim, DB-clock leases, heartbeat, current-job ownership and project/scene revisions reject stale writes. Cancellation forbids late writes; retry is explicit. Original assets are immutable. The Archify viewer's fixed controls fall back to English for Korean-authored diagrams. - -The [2026-09-21 architecture and goal audit](docs/operations/evidence/storyboard-restore-20260920/architecture-goal-audit-20260921.md) joins the MLX, queue/lease, FSM, memory, map/filter, and hosted-feed readback evidence without treating hosted writes or deployment as complete. - -**Local evidence (2026-09-20):** real-model project v12 → edit v13 → scene restore v14 → full undo v15 → full restore v16. All 20 exported PNG/WebP files matched their original hashes and decoded successfully; these three restores created zero jobs. See the [verification report and responsive screenshots](docs/operations/evidence/storyboard-restore-20260920/README.md). Local migration and UI verification do not establish hosted migration, external Web review or production deployment. +| Diagram | Open | +| --- | --- | +| System map | [tzudong-system-architecture.html](docs/architecture/tzudong-system-architecture.html) | +| Map discovery | [map-discovery.html](docs/architecture/map-discovery/map-discovery.html) | +| Evidence pipeline | [tzudong-data-pipeline.html](docs/architecture/data-pipeline/tzudong-data-pipeline.html) | +| Storyboard workflow | [storyboard-local-mlx.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.html) | -**Map evidence (2026-09-20, updated):** Seoul cluster expand kept individual markers and opened the restaurant list (66 places after the hot-view filter). YouTube play control stayed centered on 16:9 `sddefault`. GPS remains fail-closed with `DEVICE_LOCATION_OPERATOR_EVIDENCE_REQUIRED`. In the explicit hosted development mode, local `/feed` was rechecked on 2026-09-21 against the hosted Supabase project: the public feed rendered two verified reviews for 데일리픽스 and 스시린 under `쯔동마스터`, with no Nightly identity. The default local command remains isolated from hosted data. See [hosted readback and limitations](docs/operations/evidence/storyboard-restore-20260920/feed-local-readback-20260921.md). Theme chips live-rechecked: hot-view 125 (Seoul cluster 65 → list 66), comment-hot 125 (Seoul 65), fan-signal 63 (Seoul 32), recent-video 32 (Seoul 17), repeat 20 (Seoul cluster 12 → list 13), using hosted-read YouTube KPI snapshots copied locally (not invented). Public overlay/map panel elevation is `shadow-sm`; dropdown menus keep their `shadow-2xl` contract. Mobile `/feed` hides the write FAB while login or detail sheets are open. Desktop/390/320 screenshots: [public UI evidence](docs/operations/evidence/storyboard-restore-20260920/). Hosted deployment and protected promotion are still separate. +Promotion is `develop -> data -> main`. A production build runs only when `TZUDONG_APPROVED_PRODUCTION_SHA` matches that commit. Detail lives in [docs/README.md](docs/README.md). ### Measured query and normalization optimizations (2026-09-21) diff --git a/apps/web/.env.example b/apps/web/.env.example index 6c3716e7c4..8006a22d82 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -121,18 +121,6 @@ THUMBNAIL_AGENT_ROOT=../../backend/thumbnail-agent THUMBNAIL_AGENT_PYTHON=python3 THUMBNAIL_AGENT_RUNTIME=local_graph THUMBNAIL_AGENT_TIMEOUT_MS=120000 -# Device-location release evidence (server-only; default is blocked). -# Exact evidence references are operator inputs, not a legal-compliance flag. -DEVICE_LOCATION_RELEASE_DECISION= -# Allowed only: not_applicable_verified | filing_receipt_verified -DEVICE_LOCATION_EXTERNAL_STATUS= -# Each evidence reference is a SHA-256 digest (64 lowercase hex characters). -DEVICE_LOCATION_OPERATOR_EVIDENCE_HASH= -DEVICE_LOCATION_PROVIDER_EVIDENCE_HASH= -DEVICE_LOCATION_EXTERNAL_EVIDENCE_HASH= -DEVICE_LOCATION_LEGAL_EVIDENCE_HASH= -# Canonical UTC ISO timestamp, for example 2026-07-12T00:00:00.000Z -DEVICE_LOCATION_RELEASE_CONFIRMED_AT= # Keyed minimization for request IP/user-agent audit references (server-only, >=32 bytes). PRIVACY_AUDIT_HASH_KEY= # Durable account-deletion worker: independent server-only capability (minimum 32 UTF-8 bytes). diff --git a/apps/web/app/admin/banners/page.tsx b/apps/web/app/admin/banners/page.tsx index f4f5d1ca57..6eee4c54eb 100644 --- a/apps/web/app/admin/banners/page.tsx +++ b/apps/web/app/admin/banners/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useRef, useMemo, Suspense, useEffect } from 'react'; +import { useState, useRef, useMemo, Suspense, useEffect, useLayoutEffect } from 'react'; +import { useFilledSkeletonCount } from '@/lib/use-filled-skeleton-count'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/contexts/AuthContext'; @@ -120,13 +121,14 @@ const isNestedUploadInteractiveTarget = (target: EventTarget | null) => { type BannerManagementPageWrapperProps = { embedded?: boolean; + onInitialContentReady?: () => void; }; // Suspense 래퍼 -function BannerManagementPageWrapper({ embedded = false }: BannerManagementPageWrapperProps = {}) { +function BannerManagementPageWrapper({ embedded = false, onInitialContentReady }: BannerManagementPageWrapperProps = {}) { return ( - + ); } @@ -140,12 +142,19 @@ BannerManagementRoutePage.Embedded = BannerManagementPageWrapper; export default BannerManagementRoutePage; -function BannerManagementPage({ embedded }: Required) { +function BannerManagementPage({ embedded, onInitialContentReady }: BannerManagementPageWrapperProps & { embedded: boolean }) { const router = useRouter(); const { user, isAdmin, isLoading: authLoading } = useAuth(); // 배너 데이터 const { data: banners = [], isLoading: bannersLoading } = useAdBannersAdmin(); + const initialLoadPendingRef = useRef(true); + if (initialLoadPendingRef.current && !authLoading && !bannersLoading) initialLoadPendingRef.current = false; + useLayoutEffect(() => { + if (!onInitialContentReady || authLoading || initialLoadPendingRef.current) return; + onInitialContentReady(); + }, [authLoading, bannersLoading, onInitialContentReady]); + const bannerListSkeleton = useFilledSkeletonCount(88, 5); const createBanner = useCreateAdBanner(); const updateBanner = useUpdateAdBanner(); const deleteBanner = useDeleteAdBanner(); @@ -547,7 +556,8 @@ function BannerManagementPage({ embedded }: Required )} @@ -592,11 +602,11 @@ function BannerManagementPage({ embedded }: Required -
+
{bannersLoading ? (
배너 목록 데이터를 불러오는 중입니다. - {Array.from({ length: 5 }).map((_, index) => )} + {Array.from({ length: bannerListSkeleton.count }).map((_, index) => )}
) : sortedBanners.length === 0 ? ( diff --git a/apps/web/app/admin/evaluations/page.tsx b/apps/web/app/admin/evaluations/page.tsx index 3ea9da9af8..317352262e 100644 --- a/apps/web/app/admin/evaluations/page.tsx +++ b/apps/web/app/admin/evaluations/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, useEffect, useMemo, useCallback, useRef, Suspense } from 'react'; +import { useState, useEffect, useLayoutEffect, useMemo, useCallback, useRef, Suspense } from 'react'; +import { useFilledSkeletonCount } from '@/lib/use-filled-skeleton-count'; import { useRouter, useSearchParams } from 'next/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; @@ -1314,6 +1315,7 @@ type AdminEvaluationPageWrapperProps = { embedded?: boolean; initialView?: 'evaluations' | 'submissions'; initialSubmissionTab?: 'new' | 'edit' | 'recommend' | 'reviews'; + onInitialContentReady?: () => void; }; // Suspense 래퍼 컴포넌트 @@ -1321,6 +1323,7 @@ function AdminEvaluationPageWrapper({ embedded = false, initialView = 'evaluations', initialSubmissionTab, + onInitialContentReady, }: AdminEvaluationPageWrapperProps = {}) { return ( }> @@ -1328,6 +1331,7 @@ function AdminEvaluationPageWrapper({ embedded={embedded} initialView={initialView} initialSubmissionTab={initialSubmissionTab} + onInitialContentReady={onInitialContentReady} /> ); @@ -1402,10 +1406,10 @@ function AdminEvaluationStaticMobileLoadingControls() { ); } -function AdminEvaluationStaticCardSkeleton() { +function AdminEvaluationStaticCardSkeleton({ count }: { count: number }) { return ( -