diff --git a/LOG.md b/LOG.md index 109a793..6ec1f66 100644 --- a/LOG.md +++ b/LOG.md @@ -2,6 +2,11 @@ ## 2026-05-29 +- Added a category post-list page size control with 9/18/27 options, defaulting to 9 and persisting the user's choice in `localStorage` under `categoryPageSize`. +- Wired the selected category page size into the React Query key and `getPostsByCategory` request size, resetting to page 1 when the size changes while preserving search behavior. +- Adjusted the category page toolbar so search, page-size controls, and grid/list controls stack cleanly on narrow screens and align in one row on desktop. +- Validation: `npm run lint` passed and `npm run build` passed. Static verification confirms the category query uses `pageSize` and the page-size selector persists to `localStorage`; interactive browser verification was skipped because no local browser/Playwright automation runtime is available in this environment. +- Recommended next task: verify the category toolbar in a real browser after deploy, especially mobile widths and the 18/27 page-size transitions. - Investigated Google Search Console crawlability concerns for the live site. - Checked live `robots.txt`, `sitemap.xml`, homepage, sample post pages, and backend post API with a Googlebot user agent; public post URLs in the sitemap returned 200 with no `X-Robots-Tag` or meta robots block. - Confirmed the non-www production host `blog.wypark.me` is the relevant host; its home/archive initial HTML currently contains no `/posts/` links because public lists are client-side rendered. @@ -23,3 +28,7 @@ - Validation: `npm run lint` passed, `npm run build` passed. The build logged a sitemap fetch warning because the backend API at the local default was not reachable, but the command completed successfully. - Browser verification: previously started this app on `http://localhost:3100` because ports 3000 and 3001 were already occupied; confirmed desktop background gradients, menu bar, Dock, and window surfaces on `/`, `/archive`, and `/login`. For this follow-up, the dev/standalone server started successfully in foreground but exited when launched as a background non-interactive process, so browser verification was skipped after lint/build passed. - Recommended next task: review the remaining older Korean strings in admin/editor flows and normalize any mojibake that predates this redesign. +- Unified the macOS-inspired layer system by splitting window, card, control, sidebar, menu bar, and Dock border/shadow/blur tokens, with stronger dark-mode card separation. +- Applied the layer tokens across shared surfaces, the menu bar, Dock, sidebar controls, search/toggles, home dashboard panels, reader navigation cards, and admin dashboard/editor panels. +- Validation: `npm run lint` passed, `npm run build` passed, and in-app browser verification on `http://localhost:3100/` confirmed distinct light/dark computed border, shadow, and backdrop blur values for the menu bar, main window, internal cards, and Dock. +- Recommended next task: do a visual pass on lower-traffic admin list/modal screens and convert any remaining ad hoc hover fills to the shared card/control tokens. diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7f9006f..d026b24 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -93,7 +93,7 @@ function RecentPostRow({ post }: { post: Post }) { return (

{post.title}

@@ -123,7 +123,7 @@ function RecentCommentRow({ comment }: { comment: AdminComment }) { return ( {content} diff --git a/src/app/category/[id]/page.tsx b/src/app/category/[id]/page.tsx index fac3ff1..77787b1 100644 --- a/src/app/category/[id]/page.tsx +++ b/src/app/category/[id]/page.tsx @@ -9,10 +9,23 @@ import PostCard from '@/components/post/PostCard'; import PostListItem from '@/components/post/PostListItem'; import PostSearch from '@/components/post/PostSearch'; import EmptyState from '@/components/ui/EmptyState'; +import SegmentedControl from '@/components/ui/SegmentedControl'; import Surface from '@/components/ui/Surface'; import WindowSurface from '@/components/ui/WindowSurface'; -const PAGE_SIZE = 10; +const PAGE_SIZE_OPTIONS = [ + { label: '9개', value: '9' }, + { label: '18개', value: '18' }, + { label: '27개', value: '27' }, +] as const; +const DEFAULT_PAGE_SIZE = '9'; +const CATEGORY_PAGE_SIZE_STORAGE_KEY = 'categoryPageSize'; + +type PageSizeOption = (typeof PAGE_SIZE_OPTIONS)[number]['value']; + +const isPageSizeOption = (value: string | null): value is PageSizeOption => { + return PAGE_SIZE_OPTIONS.some((option) => option.value === value); +}; const isNoticeCategoryName = (categoryName: string) => { const normalizedName = categoryName.toLowerCase(); @@ -27,6 +40,12 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string const [page, setPage] = useState(0); const [keyword, setKeyword] = useState(''); + const [pageSize, setPageSize] = useState(() => { + if (typeof window === 'undefined') return DEFAULT_PAGE_SIZE; + + const savedSize = localStorage.getItem(CATEGORY_PAGE_SIZE_STORAGE_KEY); + return isPageSizeOption(savedSize) ? savedSize : DEFAULT_PAGE_SIZE; + }); const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => { if (isNoticeCategory || typeof window === 'undefined') return isNoticeCategory ? 'list' : 'grid'; @@ -47,9 +66,15 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string setPage(0); }; + const handlePageSizeChange = (nextSize: PageSizeOption) => { + setPage(0); + setPageSize(nextSize); + localStorage.setItem(CATEGORY_PAGE_SIZE_STORAGE_KEY, nextSize); + }; + const { data: postsData, isLoading, error, isPlaceholderData } = useQuery({ - queryKey: ['posts', 'category', apiCategoryName, page, keyword], - queryFn: () => getPostsByCategory(apiCategoryName, page, PAGE_SIZE, keyword), + queryKey: ['posts', 'category', apiCategoryName, page, pageSize, keyword], + queryFn: () => getPostsByCategory(apiCategoryName, page, Number(pageSize), keyword), placeholderData: (previousData) => previousData, }); @@ -96,22 +121,30 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string 글 목록 -
+
-
+ + +
@@ -479,7 +479,7 @@ export default function AdminPostEditor({ editSlug }: AdminPostEditorProps) { @@ -656,7 +656,7 @@ export default function AdminPostEditor({ editSlug }: AdminPostEditorProps) { ) : (
    {drafts.map((draft) => ( -
  • +
  • diff --git a/src/components/layout/DesktopDock.tsx b/src/components/layout/DesktopDock.tsx index 8bbb343..0c0c4a6 100644 --- a/src/components/layout/DesktopDock.tsx +++ b/src/components/layout/DesktopDock.tsx @@ -109,14 +109,14 @@ export default function DesktopDock({ isSidebarCollapsed }: DesktopDockProps) { const Icon = item.icon; const active = item.isActive?.(pathname) ?? false; const itemClass = clsx( - 'group relative flex h-10 w-10 items-center justify-center rounded-lg border border-white/20 bg-[var(--color-control)] text-[var(--color-text-muted)] shadow-[var(--shadow-control)] transition duration-150 hover:-translate-y-1 hover:bg-[var(--window-bg-strong)] hover:text-[var(--color-text)] focus-visible:-translate-y-1 sm:h-12 sm:w-12', - active && 'bg-[var(--window-bg-strong)] text-[var(--color-accent)] ring-1 ring-[var(--color-accent-soft)]', + 'group relative flex h-10 w-10 items-center justify-center rounded-lg border border-[var(--control-border)] bg-[var(--color-control)] text-[var(--color-text-muted)] shadow-[var(--shadow-control)] transition duration-150 hover:-translate-y-1 hover:bg-[var(--card-bg-strong)] hover:text-[var(--color-text)] focus-visible:-translate-y-1 sm:h-12 sm:w-12', + active && 'bg-[var(--card-bg-strong)] text-[var(--color-accent)] ring-1 ring-[var(--color-accent-soft)]', ); const content = ( <> - + {item.label} {active && ( @@ -162,7 +162,7 @@ export default function DesktopDock({ isSidebarCollapsed }: DesktopDockProps) { isSidebarCollapsed ? 'md:left-[calc(50%+2.5rem)]' : 'md:left-[calc(50%+9rem)]', )} > -
    +
    {items.map(renderItem)}
    diff --git a/src/components/layout/DesktopMenuBar.tsx b/src/components/layout/DesktopMenuBar.tsx index 6ea69d8..90f9faa 100644 --- a/src/components/layout/DesktopMenuBar.tsx +++ b/src/components/layout/DesktopMenuBar.tsx @@ -58,7 +58,7 @@ export default function DesktopMenuBar({ isSidebarCollapsed }: DesktopMenuBarPro isSidebarCollapsed ? 'md:left-24' : 'md:left-[19rem]', )} > -
    +
    WYPark diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index bb453df..14ee966 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -74,7 +74,7 @@ function CategoryItem({ category, depth, onNavigate, pathname }: CategoryItemPro 'flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-all', isActive ? 'bg-[var(--color-accent-soft)] font-semibold text-[var(--color-accent)] shadow-sm' - : 'text-[var(--color-text-muted)] hover:bg-black/[0.04] hover:text-[var(--color-text)] dark:hover:bg-white/10', + : 'text-[var(--color-text-muted)] hover:bg-[var(--card-bg)] hover:text-[var(--color-text)]', )} style={{ marginLeft: `${depth * 8}px` }} > @@ -95,7 +95,7 @@ function CategoryItem({ category, depth, onNavigate, pathname }: CategoryItemPro event.stopPropagation(); setIsExpanded((previous) => !previous); }} - className="ml-1 rounded-full p-1 transition-colors hover:bg-black/[0.06] dark:hover:bg-white/10" + className="ml-1 rounded-full p-1 transition-colors hover:bg-[var(--card-bg)]" aria-label={isChildrenVisible ? `${category.name} 접기` : `${category.name} 펼치기`} > setIsOpen((previous) => !previous)} - className="fixed left-4 top-4 z-50 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] p-2 shadow-[var(--shadow-control)] backdrop-blur-xl transition-colors hover:bg-[var(--color-surface-strong)] md:hidden" + className="fixed left-4 top-4 z-50 rounded-full border border-[var(--control-border)] bg-[var(--color-control)] p-2 shadow-[var(--shadow-control)] backdrop-blur-[18px] transition-colors hover:bg-[var(--card-bg-strong)] md:hidden" aria-label={isOpen ? '사이드바 닫기' : '사이드바 열기'} > {isOpen ? : } @@ -189,7 +189,7 @@ function SidebarContent({