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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/web/app/admin/banners/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,22 @@ function BannerListItemSkeleton({ index }: { index: number }) {
<p className="min-w-0 flex-1 truncate text-sm font-bold text-foreground">
<Skeleton className={cn("h-5 rounded-full motion-reduce:animate-none", index % 2 === 0 ? "w-32" : "w-24")} />
</p>
<Badge variant="outline" className="shrink-0 rounded-full text-[10px]">
<Badge variant="outline" className="shrink-0 rounded-full text-2xs">
<Skeleton className="h-3 w-6 rounded-full motion-reduce:animate-none" />
</Badge>
</div>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
<Skeleton className="h-4 w-4/5 rounded-full motion-reduce:animate-none" />
</p>
<div className="mt-1 flex flex-wrap gap-1">
<Badge variant="secondary" className="rounded-full text-[10px]">
<Badge variant="secondary" className="rounded-full text-2xs">
<Skeleton className="h-3 w-14 rounded-full motion-reduce:animate-none" />
</Badge>
<Badge variant="secondary" className="rounded-full text-[10px]">
<Badge variant="secondary" className="rounded-full text-2xs">
<Skeleton className="h-3 w-12 rounded-full motion-reduce:animate-none" />
</Badge>
{index % 2 === 0 && (
<span className="inline-flex items-center text-[10px] text-primary">
<span className="inline-flex items-center text-2xs text-primary">
<ExternalLink className="mr-0.5 h-3 w-3" aria-hidden="true" />
<Skeleton className="h-3 w-6 rounded-full motion-reduce:animate-none" />
</span>
Expand Down Expand Up @@ -626,13 +626,13 @@ function BannerManagementPage({ embedded }: Required<BannerManagementPageWrapper
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-start justify-between gap-2">
<p className="min-w-0 truncate text-sm font-bold text-foreground">{banner.title}</p>
<Badge variant={banner.is_active ? "default" : "outline"} className="shrink-0 rounded-full text-[10px]">{banner.is_active ? '활성' : '비활성'}</Badge>
<Badge variant={banner.is_active ? "default" : "outline"} className="shrink-0 rounded-full text-2xs">{banner.is_active ? '활성' : '비활성'}</Badge>
</div>
<p className="mt-0.5 truncate text-xs text-muted-foreground">{banner.description || '설명 없음'} · 우선순위 {banner.priority}</p>
<div className="mt-1 flex min-w-0 flex-nowrap gap-1 overflow-x-auto scrollbar-hide [scrollbar-width:none] sm:flex-wrap sm:overflow-visible [&::-webkit-scrollbar]:hidden">
{banner.display_target.includes('sidebar') && <Badge variant="secondary" className="shrink-0 rounded-full text-[10px]">데스크톱 배너</Badge>}
{banner.display_target.includes('mobile_popup') && <Badge variant="secondary" className="shrink-0 rounded-full text-[10px]">모바일 팝업</Badge>}
{resolvedUrls?.link_url && <span className="inline-flex shrink-0 items-center text-[10px] text-primary"><ExternalLink className="mr-0.5 h-3 w-3" aria-hidden="true" />링크</span>}
{banner.display_target.includes('sidebar') && <Badge variant="secondary" className="shrink-0 rounded-full text-2xs">데스크톱 배너</Badge>}
{banner.display_target.includes('mobile_popup') && <Badge variant="secondary" className="shrink-0 rounded-full text-2xs">모바일 팝업</Badge>}
{resolvedUrls?.link_url && <span className="inline-flex shrink-0 items-center text-2xs text-primary"><ExternalLink className="mr-0.5 h-3 w-3" aria-hidden="true" />링크</span>}
</div>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions apps/web/app/admin/evaluations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3522,7 +3522,11 @@ function AdminEvaluationPage({
<div className="flex items-center gap-2">
<AdminEvaluationTitleIcon embedded={embedded} />
<h1 className={embedded ? "whitespace-nowrap bg-gradient-primary bg-clip-text text-base font-bold text-transparent" : "whitespace-nowrap bg-gradient-primary bg-clip-text text-lg font-bold text-transparent sm:text-2xl"}>
관리자 데이터 검수
{embeddedModuleId === 'submissions'
? '제보 관리'
: embeddedModuleId === 'reviews'
? '리뷰 관리'
: '관리자 데이터 검수'}
</h1>
</div>
{deepLinkFilter && (
Expand Down Expand Up @@ -3564,6 +3568,7 @@ function AdminEvaluationPage({
stats={stats}
selectedStatuses={selectedStatuses}
onSelectStatuses={setSelectedStatuses}
showStatusChips={!showSubmissionView}
>
<div className="ml-auto flex items-center justify-end gap-1.5 lg:gap-1" data-admin-evaluation-view-actions="top-right" data-admin-module-actions={embedded ? "top-right" : undefined}>
{canSwitchEvaluationView && (
Expand Down Expand Up @@ -3618,7 +3623,7 @@ function AdminEvaluationPage({
<span className="lg:hidden">제보</span>
{totalPendingCount > 0 && (
<>
<span className="inline-flex md:inline-flex min-w-[18px] items-center justify-center rounded-full bg-red-500 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white lg:hidden">
<span className="inline-flex md:inline-flex min-w-[18px] items-center justify-center rounded-full bg-red-500 px-1.5 py-0.5 text-2xs font-semibold leading-none text-white lg:hidden">
{totalPendingCount > 99 ? '99+' : totalPendingCount}
</span>
<span className="absolute -right-1 top-0 hidden h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs text-white lg:flex">
Expand Down
44 changes: 44 additions & 0 deletions apps/web/app/api/admin/banners/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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;

try {
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 ?? [] });
} catch {
return NextResponse.json({ code: "banner_read_failed" }, { status: 500 });
}

}
15 changes: 12 additions & 3 deletions apps/web/app/app-globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/global-map/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ export default function GlobalMapPage() {
{categories.length > 0 && (
<span className="mt-2 flex flex-wrap gap-1">
{categories.slice(0, 3).map((category) => (
<Badge key={category} variant="secondary" className="text-[10px]">
<Badge key={category} variant="secondary" className="text-2xs">
{category}
</Badge>
))}
Expand Down
10 changes: 10 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions apps/web/app/home-app-globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
@source "../lib/naver-map-overlay-position-helpers.ts";
@source "../lib/naver-map-overlay-timings.ts";

html[data-home-chrome-ready="true"] [data-home-static-skeleton] {
display: none;
}

@media (max-width: 1279px) {
html[data-mobile-search-open] .mobile-map-status-badge {
display: none;
Expand Down Expand Up @@ -171,6 +175,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 {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/home-deferred-globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/home-detail-globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 29 additions & 2 deletions apps/web/app/home-runtime-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ function MobileHomeLayout({ children }: { children: ReactNode }) {
<a href="#main-content" className="skip-link">
본문 바로가기
</a>
<main id="main-content" className="flex-1 relative overflow-hidden" aria-label="쯔동여지도 지도 본문">
<main id="main-content" className="relative flex-1 overflow-hidden" aria-label="쯔동여지도 지도 본문">
<HomeStaticSkeleton />
<div className="h-full w-full">
{children}
</div>
Expand Down Expand Up @@ -317,13 +318,39 @@ function HomeLayoutContent({ children }: { children: ReactNode }) {
);
}

function HomeStaticSkeleton() {
return (
<div
className="pointer-events-none absolute inset-0 z-[20]"
data-home-static-skeleton="true"
aria-hidden="true"
>
<div className="h-full max-xl:hidden">
<div className="h-full w-[min(392px,calc(100vw-32px))] border-r border-border bg-card p-4">
<div className="h-11 animate-pulse rounded-full bg-muted" />
<div className="mt-4 space-y-3">
{Array.from({ length: 6 }, (_, index) => (
<div key={index} className="h-16 animate-pulse rounded-xl bg-muted" />
Comment on lines +330 to +333

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable skeleton pulsing for reduced-motion users

The new initial-home skeleton applies animate-pulse to every placeholder without a motion-reduce:animate-none override, while the home-specific stylesheet's blanket reduced-motion rule is commented out. Users who request reduced motion therefore still receive several continuously pulsing elements during startup; add the reduced-motion utility or an equivalent active media rule to these placeholders.

Useful? React with 👍 / 👎.

))}
</div>
</div>
</div>
<div className="xl:hidden">
<div className="mx-3 mt-[calc(env(safe-area-inset-top)+10px)] h-12 animate-pulse rounded-full border border-border bg-muted" />
<div className="absolute inset-x-3 bottom-[calc(env(safe-area-inset-bottom)+0.75rem)] h-14 animate-pulse rounded-2xl bg-muted" />
</div>
</div>
);
}

function HomeRuntimePendingShell({ children }: { children: ReactNode }) {
return (
<div className="flex flex-col bg-background text-foreground" style={{ height: 'var(--full-height, 100vh)' }}>
<a href="#main-content" className="skip-link">
본문 바로가기
</a>
<main id="main-content" className="h-full min-h-0 w-full flex-1 bg-background" aria-label="쯔동여지도 지도 본문">
<main id="main-content" className="relative h-full min-h-0 w-full flex-1 bg-background" aria-label="쯔동여지도 지도 본문">
<HomeStaticSkeleton />
{children}
</main>
</div>
Expand Down
Loading
Loading