From c774351a36423d056cae4116692ab1cb2ccb0cee Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Thu, 24 Sep 2026 17:13:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=B2=AB=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=EC=A0=81=20=EC=8A=A4=EC=BC=88=EB=A0=88?= =?UTF-8?q?=ED=86=A4=EC=9C=BC=EB=A1=9C=20=EA=B3=A0=EC=A0=95=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=8A=A4=ED=83=80=EC=9D=BC=EA=B3=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=EC=A7=80=ED=91=9C=EB=A5=BC=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 홈은 뷰포트가 정해지기 전에 같은 크기의 스켈레톤을 그리고, 테두리·글자 스케일·관리자 배너 조회와 로컬 3000 포트를 맞춘다. Co-authored-by: Cursor --- apps/web/app/admin/banners/page.tsx | 16 +- apps/web/app/admin/evaluations/page.tsx | 9 +- apps/web/app/api/admin/banners/route.ts | 39 +++ apps/web/app/app-globals.css | 15 +- apps/web/app/global-map/page.tsx | 2 +- apps/web/app/globals.css | 10 + apps/web/app/home-app-globals.css | 8 + apps/web/app/home-deferred-globals.css | 8 + apps/web/app/home-detail-globals.css | 8 + apps/web/app/home-runtime-shell.tsx | 31 ++- apps/web/app/insights/insights-client.tsx | 28 +- apps/web/app/leaderboard/page.tsx | 2 + apps/web/app/mypage/mypage-layout-content.tsx | 3 + apps/web/app/mypage/profile/page.tsx | 20 +- apps/web/app/privacy/page.tsx | 7 +- apps/web/app/stamp/page.tsx | 4 +- .../components/admin/AdminConsoleOverview.tsx | 259 ++++++++++-------- .../admin/AdminEmbeddedModuleShell.tsx | 2 +- .../admin/AdminOverviewDashboard.tsx | 30 +- .../components/admin/AdminRestaurantModal.tsx | 6 +- .../AdminRestaurantRefreshHistoryPanel.tsx | 8 +- .../web/components/admin/AdminReviewPanel.tsx | 4 +- apps/web/components/admin/AdminUsersPanel.tsx | 4 +- apps/web/components/admin/CategorySidebar.tsx | 16 +- .../components/admin/EvaluationDetailView.tsx | 24 +- .../components/admin/EvaluationSlideView.tsx | 2 +- .../components/admin/EvaluationTableNew.tsx | 18 +- .../components/admin/SubmissionDetailView.tsx | 20 +- .../components/admin/SubmissionListView.tsx | 48 ++-- .../components/admin/TrendProposalQueue.tsx | 22 +- .../admin/evaluation-status-filter-options.ts | 2 +- .../admin/pipeline/AdminPipelineDashboard.tsx | 2 +- .../storyboard/AdminStoryboardGenerator.tsx | 132 ++++----- .../system-status/AdminSystemStatusCenter.tsx | 12 +- .../AdminYoutubeThumbnailGenerator.tsx | 94 +++---- .../announcement/AnnouncementPanel.tsx | 2 +- apps/web/components/feed/FeedContent.tsx | 2 + .../web/components/filters/CategoryFilter.tsx | 4 +- .../home/DesktopLeftPanelBookmarks.tsx | 6 +- .../home/DesktopLeftPanelMapHome.tsx | 14 +- .../home/DesktopLeftPanelNotifications.tsx | 8 +- apps/web/components/home/HomeMapUserMenu.tsx | 4 +- .../home/MobileBookmarkMenuButton.tsx | 4 +- .../components/home/MobileControlOverlay.tsx | 10 +- .../home/MobileNotificationMenuButton.tsx | 4 +- .../home/home-desktop-control-panel.tsx | 4 +- apps/web/components/layout/Header.tsx | 8 +- .../layout/HeaderBookmarkMenuButton.tsx | 4 +- .../leaderboard/LeaderboardList.tsx | 2 +- .../web/components/map/map-overlay-notice.tsx | 4 +- .../components/modals/EditRestaurantModal.tsx | 6 +- .../modals/RestaurantSubmissionModal.tsx | 13 +- .../components/mypage/MyPageSectionFrame.tsx | 2 +- apps/web/components/mypage/MyPageSidebar.tsx | 7 +- .../components/mypage/MyPageTopActions.tsx | 4 +- .../components/overlay-pages/StampOverlay.tsx | 2 +- .../components/profile/UserProfilePanel.tsx | 6 +- .../restaurant/RestaurantDetailPanel.tsx | 12 +- apps/web/components/reviews/ReviewCard.tsx | 4 +- .../components/reviews/ReviewEditModal.tsx | 4 +- apps/web/components/reviews/ReviewModal.tsx | 34 +-- .../stamp/RestaurantReviewsPanel.tsx | 2 +- apps/web/components/stamp/StampCard.tsx | 26 +- apps/web/components/ui/bookmark-button.tsx | 2 +- apps/web/components/ui/card.tsx | 2 +- apps/web/components/ui/dialog.tsx | 4 +- apps/web/components/ui/mobile-sheet-frame.tsx | 4 +- apps/web/components/ui/sheet.tsx | 2 +- apps/web/components/ui/toast.tsx | 4 +- apps/web/hooks/use-ad-banners.tsx | 23 +- apps/web/package.json | 20 +- apps/web/playwright.config.ts | 2 +- apps/web/stylegallery-adoption.v1.json | 6 +- apps/web/tailwind.config.ts | 3 + .../admin-console-uiux-source.test.ts | 39 ++- ...n-evaluation-status-filter-options.test.ts | 4 +- ...er-bookmark-notification-ux-source.test.ts | 16 +- .../tests-unit/local-supabase-runtime.test.ts | 8 +- .../mobile-desktop-parity-source.test.ts | 4 +- .../mypage-mobile-cleanup-contract.test.ts | 8 +- .../naver-live-provider-smoke-source.test.ts | 2 +- .../public-surface-design-contract.test.ts | 2 +- .../standalone-static-source.test.ts | 2 +- .../stylegallery-primitive-taxonomy.test.ts | 6 +- .../web-quality-performance-source.test.ts | 8 +- apps/web/tests/mobile-home-map-helpers.ts | 2 +- apps/web/tests/nightly/nightly-test.ts | 4 +- docs/product/DESIGN.md | 13 +- 88 files changed, 739 insertions(+), 568 deletions(-) create mode 100644 apps/web/app/api/admin/banners/route.ts diff --git a/apps/web/app/admin/banners/page.tsx b/apps/web/app/admin/banners/page.tsx index a6f17c84fa..f4f5d1ca57 100644 --- a/apps/web/app/admin/banners/page.tsx +++ b/apps/web/app/admin/banners/page.tsx @@ -69,7 +69,7 @@ function BannerListItemSkeleton({ index }: { index: number }) {

- + @@ -77,14 +77,14 @@ function BannerListItemSkeleton({ index }: { index: number }) {

- + - + {index % 2 === 0 && ( - + @@ -626,13 +626,13 @@ function BannerManagementPage({ embedded }: Required

{banner.title}

- {banner.is_active ? '활성' : '비활성'} + {banner.is_active ? '활성' : '비활성'}

{banner.description || '설명 없음'} · 우선순위 {banner.priority}

- {banner.display_target.includes('sidebar') && 데스크톱 배너} - {banner.display_target.includes('mobile_popup') && 모바일 팝업} - {resolvedUrls?.link_url && } + {banner.display_target.includes('sidebar') && 데스크톱 배너} + {banner.display_target.includes('mobile_popup') && 모바일 팝업} + {resolvedUrls?.link_url && }
diff --git a/apps/web/app/admin/evaluations/page.tsx b/apps/web/app/admin/evaluations/page.tsx index b12b53550c..3ea9da9af8 100644 --- a/apps/web/app/admin/evaluations/page.tsx +++ b/apps/web/app/admin/evaluations/page.tsx @@ -3522,7 +3522,11 @@ function AdminEvaluationPage({

- 관리자 데이터 검수 + {embeddedModuleId === 'submissions' + ? '제보 관리' + : embeddedModuleId === 'reviews' + ? '리뷰 관리' + : '관리자 데이터 검수'}

{deepLinkFilter && ( @@ -3564,6 +3568,7 @@ function AdminEvaluationPage({ stats={stats} selectedStatuses={selectedStatuses} onSelectStatuses={setSelectedStatuses} + showStatusChips={!showSubmissionView} >
{canSwitchEvaluationView && ( @@ -3618,7 +3623,7 @@ function AdminEvaluationPage({ 제보 {totalPendingCount > 0 && ( <> - + {totalPendingCount > 99 ? '99+' : totalPendingCount} diff --git a/apps/web/app/api/admin/banners/route.ts b/apps/web/app/api/admin/banners/route.ts new file mode 100644 index 0000000000..be01e6ffce --- /dev/null +++ b/apps/web/app/api/admin/banners/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; + +import { requireAdmin } from "@/lib/auth/require-admin"; +import { createSupabaseServiceRoleClient } from "@/lib/supabase/service-role"; + +export const runtime = "nodejs"; + +const AD_BANNER_SELECT = [ + "id", + "title", + "description", + "image_url", + "video_url", + "media_type", + "link_url", + "is_active", + "priority", + "display_target", + "created_at", + "updated_at", + "created_by", +].join(","); + +export async function GET() { + const auth = await requireAdmin(); + if (!auth.ok) return auth.response; + + const supabase = createSupabaseServiceRoleClient(); + const { data, error } = await supabase + .from("ad_banners") + .select(AD_BANNER_SELECT) + .order("priority", { ascending: false }); + + if (error) { + return NextResponse.json({ code: "banner_read_failed" }, { status: 500 }); + } + + return NextResponse.json({ banners: data ?? [] }); +} diff --git a/apps/web/app/app-globals.css b/apps/web/app/app-globals.css index 51cea9868a..331ffdc4ed 100644 --- a/apps/web/app/app-globals.css +++ b/apps/web/app/app-globals.css @@ -10,8 +10,8 @@ @source not "../{tests-unit,test,tests,fixtures}/**"; /* Tailwind v4 explicit retention for utilities assembled outside scanned route source. */ @source inline("lg:block lg:flex lg:hidden lg:table-cell"); -@source inline("lg:border-r lg:grid lg:overflow-y-auto lg:px-0 lg:sticky sm:col-span-2 sm:grid-cols-2 sm:grid-cols-5 lg:col-span-2 lg:col-span-3 lg:col-span-4 lg:col-span-5 lg:flex-row lg:grid-cols-10 lg:flex-1 lg:grid-rows-[auto_minmax(0,1.15fr)_minmax(0,1fr)] lg:grid-cols-[minmax(0,1.05fr)_minmax(360px,0.95fr)] lg:h-full lg:justify-between lg:min-h-0 lg:overflow-hidden lg:overflow-visible xl:grid-cols-3 xl:grid-cols-4 xl:grid-cols-[1.1fr_0.9fr]"); -@source inline("font-sans font-black font-extrabold gap-0 flex-nowrap justify-end md:justify-end md:p-4 md:border-y-0 overflow-visible overflow-y-visible px-0.5 grid-cols-[5.5rem_minmax(0,1fr)_3rem] grid-rows-2 grid-rows-[auto_minmax(0,1fr)_auto] grid-rows-[minmax(0,1fr)_auto] min-h-[132px] min-h-[190px] min-h-[220px] min-h-[230px] min-h-[280px] min-w-[8%] min-w-fit sm:max-w-none sm:min-w-fit sm:p-1.5 sm:p-3.5 text-[13px] text-lg sm:text-xl tracking-[-0.035em] tracking-[0.01em] tracking-[0.04em] bg-border/70 bg-muted-foreground/25 bg-muted-foreground/28 bg-muted-foreground/30 bg-muted-foreground/35 bg-muted-foreground/45 bg-muted-foreground/55 bg-muted/25 bg-muted/35 bg-muted/45 bg-teal-50 bg-teal-500/55 bg-teal-500/75 border-teal-500/25 dark:bg-muted-foreground/35 dark:bg-muted/20 dark:bg-muted/35 dark:bg-teal-400/55 dark:bg-teal-400/75 dark:bg-teal-950/35 dark:border-teal-400/30 dark:text-emerald-300 md:block md:flex md:hidden md:flex-row md:flex-wrap md:items-start md:justify-between md:gap-1 md:w-auto md:overflow-visible md:pb-0 sm:inline bg-sky-50 bg-sky-50/20 bg-sky-500 bg-teal-500 bg-rose-50 bg-rose-500 bg-orange-50 bg-amber-500 bg-amber-600 text-white text-teal-700 text-sky-700 text-rose-700 text-amber-700 border-sky-500/35 dark:bg-sky-400 dark:bg-sky-950/20 dark:bg-teal-400 dark:bg-teal-500 dark:bg-rose-400 dark:bg-rose-500 dark:bg-amber-400 dark:bg-amber-500 dark:text-white dark:text-teal-300 dark:text-sky-300 dark:text-rose-300 dark:text-amber-300 dark:border-sky-400/30 dark:border-sky-400/45"); +@source inline("lg:border-r lg:grid lg:overflow-y-auto lg:px-0 lg:sticky sm:col-span-2 sm:grid-cols-2 sm:grid-cols-5 lg:col-span-2 lg:col-span-3 lg:col-span-4 lg:col-span-5 lg:col-span-6 lg:flex-row lg:grid-cols-10 lg:flex-1 lg:grid-rows-[auto_minmax(0,1.15fr)_minmax(0,1fr)] lg:grid-cols-[minmax(0,1.05fr)_minmax(360px,0.95fr)] lg:h-full lg:justify-between lg:min-h-0 lg:overflow-hidden lg:overflow-visible xl:grid-cols-3 xl:grid-cols-4 xl:grid-cols-[1.1fr_0.9fr]"); +@source inline("font-sans font-black font-extrabold gap-0 flex-nowrap justify-end md:justify-end md:p-4 md:border-y-0 overflow-visible overflow-y-visible px-0.5 grid-cols-[5.5rem_minmax(0,1fr)_3rem] grid-rows-2 grid-rows-[auto_minmax(0,1fr)_auto] grid-rows-[minmax(0,1fr)_auto] min-h-[132px] min-h-[190px] min-h-[220px] min-h-[230px] min-h-[280px] min-w-[8%] min-w-fit sm:max-w-none sm:min-w-fit sm:p-1.5 sm:p-3.5 text-sm text-lg sm:text-xl tracking-[-0.035em] tracking-[0.01em] tracking-[0.04em] bg-border/70 bg-muted-foreground/25 bg-muted-foreground/28 bg-muted-foreground/30 bg-muted-foreground/35 bg-muted-foreground/45 bg-muted-foreground/55 bg-muted/25 bg-muted/35 bg-muted/45 bg-teal-50 bg-teal-500/55 bg-teal-500/75 border-teal-500/25 dark:bg-muted-foreground/35 dark:bg-muted/20 dark:bg-muted/35 dark:bg-teal-400/55 dark:bg-teal-400/75 dark:bg-teal-950/35 dark:border-teal-400/30 dark:text-emerald-300 md:block md:flex md:hidden md:flex-row md:flex-wrap md:items-start md:justify-between md:gap-1 md:w-auto md:overflow-visible md:pb-0 sm:inline bg-sky-50 bg-sky-50/20 bg-sky-500 bg-teal-500 bg-rose-50 bg-rose-500 bg-orange-50 bg-amber-500 bg-amber-600 text-white text-teal-700 text-sky-700 text-rose-700 text-amber-700 border-sky-500/35 dark:bg-sky-400 dark:bg-sky-950/20 dark:bg-teal-400 dark:bg-teal-500 dark:bg-rose-400 dark:bg-rose-500 dark:bg-amber-400 dark:bg-amber-500 dark:text-white dark:text-teal-300 dark:text-sky-300 dark:text-rose-300 dark:text-amber-300 dark:border-sky-400/30 dark:border-sky-400/45"); @source inline("-z-10 text-red-800 fill-red-800/20 scale-110 text-foreground/65 active:text-foreground text-[12px] font-medium font-semibold leading-none tracking-normal"); @source inline("text-yellow-500 text-muted-foreground text-amber-600"); @source inline("top-[calc(env(safe-area-inset-top)_+_126px)] bottom-[calc(var(--mobile-bottom-nav-effective-height,var(--mobile-bottom-nav-height,60px))_+_env(safe-area-inset-bottom)_+_0.75rem)] z-[61] z-[70]"); @@ -688,12 +688,21 @@ All colors MUST be HSL. @layer base { @import "../styles/light-root-tokens.css"; + *, + ::before, + ::after, + ::backdrop, + ::file-selector-button { + border-color: hsl(var(--border)); + } + :root { --font-sans: var(--font-pretendard, "Pretendard"), "Apple SD Gothic Neo", "Malgun Gothic", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; --font-display-fallback: "Noto Serif KR", "Apple SD Gothic Neo", "Malgun Gothic", ui-serif, Georgia, serif; --font-noto-serif-kr: var(--font-display, "Noto Serif KR"), "Apple SD Gothic Neo", "Malgun Gothic", ui-serif, Georgia, serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --admin-sidebar-expanded-width: 14rem; + --phi: 1.618; + --admin-sidebar-expanded-width: calc(8.75rem * var(--phi)); --admin-sidebar-expanded-max-width: min(17.5rem, 27vw); } diff --git a/apps/web/app/global-map/page.tsx b/apps/web/app/global-map/page.tsx index 33f46d6055..4a6bc9e8b6 100644 --- a/apps/web/app/global-map/page.tsx +++ b/apps/web/app/global-map/page.tsx @@ -694,7 +694,7 @@ export default function GlobalMapPage() { {categories.length > 0 && ( {categories.slice(0, 3).map((category) => ( - + {category} ))} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 1a3f89ccfe..5c964f5a7e 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -88,6 +88,16 @@ html[data-local-workspace="true"] [data-local-workspace-app="true"] { } @layer base { + /* `border` sets width only. Without this, the preflight shorthand paints every + unspecified edge with currentColor, so chips and toasts look black. */ + *, + ::before, + ::after, + ::backdrop, + ::file-selector-button { + border-color: hsl(var(--border)); + } + button, input, textarea, diff --git a/apps/web/app/home-app-globals.css b/apps/web/app/home-app-globals.css index ee635bccdf..049b654924 100644 --- a/apps/web/app/home-app-globals.css +++ b/apps/web/app/home-app-globals.css @@ -171,6 +171,14 @@ All colors MUST be HSL. @layer base { @import "../styles/light-root-tokens.css"; + + *, + ::before, + ::after, + ::backdrop, + ::file-selector-button { + border-color: hsl(var(--border)); + } } @layer base { diff --git a/apps/web/app/home-deferred-globals.css b/apps/web/app/home-deferred-globals.css index 95f19b8896..87827a15d7 100644 --- a/apps/web/app/home-deferred-globals.css +++ b/apps/web/app/home-deferred-globals.css @@ -93,6 +93,14 @@ All colors MUST be HSL. @layer base { @import "../styles/light-root-tokens.css"; + + *, + ::before, + ::after, + ::backdrop, + ::file-selector-button { + border-color: hsl(var(--border)); + } } @layer base { diff --git a/apps/web/app/home-detail-globals.css b/apps/web/app/home-detail-globals.css index 3e2828c19e..d9410a7e05 100644 --- a/apps/web/app/home-detail-globals.css +++ b/apps/web/app/home-detail-globals.css @@ -92,6 +92,14 @@ All colors MUST be HSL. @layer base { @import "../styles/light-root-tokens.css"; + + *, + ::before, + ::after, + ::backdrop, + ::file-selector-button { + border-color: hsl(var(--border)); + } } @layer base { diff --git a/apps/web/app/home-runtime-shell.tsx b/apps/web/app/home-runtime-shell.tsx index 3479425845..5b7a09eccd 100644 --- a/apps/web/app/home-runtime-shell.tsx +++ b/apps/web/app/home-runtime-shell.tsx @@ -241,7 +241,8 @@ function MobileHomeLayout({ children }: { children: ReactNode }) { 본문 바로가기 -
+
+
{children}
@@ -317,13 +318,39 @@ function HomeLayoutContent({ children }: { children: ReactNode }) { ); } +function HomeStaticSkeleton() { + return ( +