diff --git a/LOG.md b/LOG.md index a1f955d..c1bd707 100644 --- a/LOG.md +++ b/LOG.md @@ -2,6 +2,12 @@ ## 2026-06-06 +- Reworked the macOS-inspired shell into a fuller desktop illusion: full-width system menu bar, floating Finder-style sidebar window, viewport-centered Dock, desktop wallpaper layers, and desktop shortcut icons for major destinations. +- Rebuilt the public home screen as a Finder window with a locations sidebar, disk/status strip, file-like post rows, Spotlight-style search results, and stronger shared window glass treatment. +- Extended the OS treatment across archive, category, reader, login/signup, admin shell, post cards, and list rows, including file icons, Reader window framing, and cleanup of a stale mojibake notice-category check. +- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3112/`, `/archive`, and `/login` confirmed no horizontal overflow, desktop/mobile menu and Dock visibility rules, shared window surfaces, and no browser console warnings/errors. The default local API data rendered empty post states during the browser pass. +- Recommended next task: run a visual pass against `NEXT_PUBLIC_API_URL=https://blogserver.wypark.me` so populated post lists and one real reader page can be tuned with long Korean titles and real content. + - Fixed the desktop Dock so route changes from Dock menu links no longer leave focus-held open state behind when the Dock is not pinned. - Kept the saved `dock-pinned-v2` preference unchanged during navigation and only released transient Dock focus/hover state after menu clicks and pathname changes. - Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3111/` confirmed that after unpinning the Dock, clicking the Dock archive menu navigates to `/archive`, the pin button remains `aria-pressed=false`, and the Dock collapses after the pointer leaves. diff --git a/src/app/archive/page.tsx b/src/app/archive/page.tsx index 78aaca9..e1cf98d 100644 --- a/src/app/archive/page.tsx +++ b/src/app/archive/page.tsx @@ -34,8 +34,8 @@ export default async function ArchivePage() { return (
diff --git a/src/app/category/[id]/page.tsx b/src/app/category/[id]/page.tsx index 77787b1..b7123bf 100644 --- a/src/app/category/[id]/page.tsx +++ b/src/app/category/[id]/page.tsx @@ -3,7 +3,7 @@ import { use, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { clsx } from 'clsx'; -import { ChevronLeft, ChevronRight, LayoutGrid, List, Loader2, Search as SearchIcon } from 'lucide-react'; +import { ChevronLeft, ChevronRight, FolderOpen, LayoutGrid, List, Loader2, Search as SearchIcon } from 'lucide-react'; import { getPostsByCategory } from '@/api/posts'; import PostCard from '@/components/post/PostCard'; import PostListItem from '@/components/post/PostListItem'; @@ -112,12 +112,16 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string return (
-

- {apiCategoryName} +

+ + + + {apiCategoryName} 글 목록

diff --git a/src/app/globals.css b/src/app/globals.css index bbcfdf1..2fa49cc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,28 +24,34 @@ Consolas, "Liberation Mono", monospace; - --color-page: #f7f4fb; + --color-page: #edf4ff; --desktop-bg: - linear-gradient(135deg, rgba(255, 232, 238, 0.78) 0%, rgba(255, 232, 238, 0) 36%), - linear-gradient(155deg, rgba(218, 235, 255, 0.86) 6%, rgba(218, 235, 255, 0) 50%), - linear-gradient(42deg, rgba(222, 251, 236, 0.74) 48%, rgba(222, 251, 236, 0) 88%), - linear-gradient(180deg, #fbf8ff 0%, #eef5ff 100%); - --window-bg: rgba(255, 255, 255, 0.6); - --window-bg-strong: rgba(255, 255, 255, 0.8); - --window-titlebar: rgba(255, 255, 255, 0.5); - --window-border: rgba(255, 255, 255, 0.68); + linear-gradient(118deg, rgba(95, 174, 255, 0.22) 0%, rgba(95, 174, 255, 0) 35%), + linear-gradient(152deg, rgba(255, 158, 194, 0.2) 10%, rgba(255, 158, 194, 0) 48%), + linear-gradient(46deg, rgba(124, 220, 185, 0.18) 46%, rgba(124, 220, 185, 0) 82%), + linear-gradient(180deg, #f9fcff 0%, #eaf3ff 48%, #f8f2ff 100%); + --desktop-sheen: + linear-gradient(120deg, rgba(255, 255, 255, 0.48) 0 1px, transparent 1px 100%), + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.12) 0 1px, transparent 1px 18px); + --desktop-icon-bg: rgba(255, 255, 255, 0.44); + --desktop-icon-bg-hover: rgba(255, 255, 255, 0.64); + --desktop-icon-border: rgba(255, 255, 255, 0.62); + --window-bg: rgba(255, 255, 255, 0.66); + --window-bg-strong: rgba(255, 255, 255, 0.86); + --window-titlebar: rgba(255, 255, 255, 0.58); + --window-border: rgba(255, 255, 255, 0.74); --window-titlebar-border: rgba(39, 54, 82, 0.1); --card-bg: rgba(255, 255, 255, 0.46); --card-bg-strong: rgba(255, 255, 255, 0.68); --card-border: rgba(39, 54, 82, 0.11); --card-border-hover: rgba(39, 54, 82, 0.18); --control-border: rgba(39, 54, 82, 0.12); - --menubar-bg: rgba(255, 255, 255, 0.5); - --menubar-border: rgba(255, 255, 255, 0.7); - --dock-bg: rgba(248, 250, 252, 0.46); - --dock-border: rgba(255, 255, 255, 0.62); - --sidebar-bg: rgba(255, 255, 255, 0.48); - --sidebar-border: rgba(255, 255, 255, 0.6); + --menubar-bg: rgba(255, 255, 255, 0.58); + --menubar-border: rgba(255, 255, 255, 0.72); + --dock-bg: rgba(248, 250, 252, 0.54); + --dock-border: rgba(255, 255, 255, 0.68); + --sidebar-bg: rgba(255, 255, 255, 0.58); + --sidebar-border: rgba(255, 255, 255, 0.68); --color-surface: var(--card-bg); --color-surface-strong: var(--card-bg-strong); --color-control: rgba(255, 255, 255, 0.56); @@ -57,9 +63,9 @@ --color-accent-hover: #0071e3; --color-accent-soft: rgba(0, 102, 204, 0.1); --color-danger-soft: rgba(255, 59, 48, 0.1); - --shadow-window: 0 28px 80px rgba(70, 92, 130, 0.18), 0 10px 28px rgba(70, 92, 130, 0.08), 0 1px 0 rgba(255, 255, 255, 0.72) inset; - --shadow-sidebar: 16px 0 42px rgba(70, 92, 130, 0.12), -1px 0 0 rgba(255, 255, 255, 0.46) inset; - --shadow-menubar: 0 16px 40px rgba(70, 92, 130, 0.12), 0 1px 0 rgba(255, 255, 255, 0.58) inset; + --shadow-window: 0 34px 96px rgba(47, 74, 112, 0.22), 0 16px 34px rgba(47, 74, 112, 0.1), 0 1px 0 rgba(255, 255, 255, 0.8) inset; + --shadow-sidebar: 18px 20px 64px rgba(47, 74, 112, 0.16), 0 1px 0 rgba(255, 255, 255, 0.56) inset; + --shadow-menubar: 0 12px 32px rgba(47, 74, 112, 0.13), 0 1px 0 rgba(255, 255, 255, 0.62) inset; --shadow-panel: var(--shadow-sidebar); --shadow-card: 0 14px 34px rgba(70, 92, 130, 0.09), 0 1px 0 rgba(255, 255, 255, 0.5) inset; --shadow-dock: 0 28px 78px rgba(15, 23, 42, 0.18), 0 12px 32px rgba(15, 23, 42, 0.12), 0 1px 0 rgba(255, 255, 255, 0.68) inset, 0 -1px 0 rgba(15, 23, 42, 0.06) inset; @@ -78,15 +84,21 @@ } html[data-theme="dark"] { - --color-page: #11131d; + --color-page: #0c101a; --desktop-bg: - linear-gradient(135deg, rgba(88, 56, 104, 0.42) 0%, rgba(88, 56, 104, 0) 38%), - linear-gradient(150deg, rgba(43, 73, 110, 0.5) 10%, rgba(43, 73, 110, 0) 54%), - linear-gradient(42deg, rgba(38, 91, 72, 0.38) 48%, rgba(38, 91, 72, 0) 88%), - linear-gradient(180deg, #131620 0%, #07090f 100%); - --window-bg: rgba(28, 31, 42, 0.68); - --window-bg-strong: rgba(39, 42, 56, 0.84); - --window-titlebar: rgba(255, 255, 255, 0.075); + linear-gradient(118deg, rgba(44, 128, 204, 0.34) 0%, rgba(44, 128, 204, 0) 38%), + linear-gradient(150deg, rgba(125, 66, 139, 0.3) 12%, rgba(125, 66, 139, 0) 55%), + linear-gradient(46deg, rgba(30, 118, 89, 0.26) 46%, rgba(30, 118, 89, 0) 84%), + linear-gradient(180deg, #111827 0%, #080b12 100%); + --desktop-sheen: + linear-gradient(120deg, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 100%), + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.035) 0 1px, transparent 1px 18px); + --desktop-icon-bg: rgba(22, 25, 34, 0.5); + --desktop-icon-bg-hover: rgba(40, 44, 58, 0.72); + --desktop-icon-border: rgba(255, 255, 255, 0.16); + --window-bg: rgba(25, 29, 40, 0.72); + --window-bg-strong: rgba(39, 43, 58, 0.88); + --window-titlebar: rgba(255, 255, 255, 0.09); --window-border: rgba(255, 255, 255, 0.18); --window-titlebar-border: rgba(255, 255, 255, 0.13); --card-bg: rgba(43, 46, 60, 0.5); @@ -94,9 +106,9 @@ html[data-theme="dark"] { --card-border: rgba(255, 255, 255, 0.14); --card-border-hover: rgba(255, 255, 255, 0.22); --control-border: rgba(255, 255, 255, 0.13); - --menubar-bg: rgba(24, 27, 38, 0.62); + --menubar-bg: rgba(14, 17, 26, 0.68); --menubar-border: rgba(255, 255, 255, 0.16); - --dock-bg: rgba(18, 20, 28, 0.46); + --dock-bg: rgba(18, 20, 28, 0.56); --dock-border: rgba(255, 255, 255, 0.16); --sidebar-bg: rgba(21, 24, 34, 0.64); --sidebar-border: rgba(255, 255, 255, 0.14); @@ -145,6 +157,68 @@ body { text-rendering: optimizeLegibility; } +.desktop-shell { + position: relative; + isolation: isolate; + min-height: 100vh; + background: var(--desktop-bg); +} + +.desktop-shell::before, +.desktop-shell::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; +} + +.desktop-shell::before { + z-index: 0; + background: + var(--desktop-sheen), + radial-gradient(ellipse at 20% 15%, rgba(255, 255, 255, 0.42), transparent 42%), + var(--desktop-bg); +} + +.desktop-shell::after { + z-index: 0; + background-image: + repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.06) 0 1px, transparent 1px 3px), + linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.18)); + opacity: 0.32; + mix-blend-mode: soft-light; +} + +html[data-theme="dark"] .desktop-shell::after { + opacity: 0.22; +} + +.window-surface { + position: relative; +} + +.window-surface::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.42), transparent 22rem), + linear-gradient(90deg, rgba(255, 255, 255, 0.18), transparent 28%); + opacity: 0.52; +} + +html[data-theme="dark"] .window-surface::before { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.08), transparent 22rem), + linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent 28%); + opacity: 0.72; +} + +.window-surface > * { + position: relative; +} + pre, code, table { diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index f489e98..e0bafdb 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -3,7 +3,7 @@ import { Suspense, useState } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Loader2 } from 'lucide-react'; +import { Loader2, LockKeyhole, UserRound } from 'lucide-react'; import { login } from '@/api/auth'; import WindowSurface from '@/components/ui/WindowSurface'; import { useAuthStore } from '@/store/authStore'; @@ -61,12 +61,15 @@ function LoginForm() { return (
+
+ +

로그인

블로그 데스크톱으로 다시 들어갑니다.

@@ -103,7 +106,7 @@ function LoginForm() { disabled={loading} className="flex w-full items-center justify-center gap-2 rounded-lg bg-[var(--color-accent)] py-3 font-bold text-white transition-colors hover:bg-[var(--color-accent-hover)] disabled:cursor-not-allowed disabled:opacity-60" > - {loading && } + {loading ? : } 로그인 diff --git a/src/app/page.tsx b/src/app/page.tsx index 2a2c38b..0a5eba4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,9 @@ import { Archive, ChevronRight, Clock, + FileText, + FolderOpen, + HardDrive, Search, TrendingUp, } from 'lucide-react'; @@ -96,32 +99,38 @@ function SearchResults({ return ( -
-
- -

- 검색 결과 -

+
+
+
+ +
+
+

+ 검색 결과 +

+

+ 검색어 {keyword}에 대한 글 {searchTotalElements.toLocaleString()}건 +

+
-

- 검색어 {keyword}에 대한 글 {searchTotalElements.toLocaleString()}건 -

- {searchResults.length > 0 ? ( -
- {searchResults.map((post) => ( - - ))} -
- ) : ( - - )} +
+ {searchResults.length > 0 ? ( +
+ {searchResults.map((post) => ( + + ))} +
+ ) : ( + + )} +
); } @@ -180,6 +189,11 @@ function CompactPostRow({ {rank} )} + {rank === undefined && ( + + + + )}
@@ -226,7 +240,7 @@ function PostListPanel({ featured?: boolean; }) { return ( - +

{icon} @@ -259,6 +273,31 @@ function PostListPanel({ ); } +function FinderLocation({ + href, + icon, + label, + meta, +}: { + href: string; + icon: ReactNode; + label: string; + meta: string; +}) { + return ( + + {icon} + {label} + + {meta} + + + ); +} + export default async function Home({ searchParams }: HomePageProps) { const keyword = await getSearchKeyword(searchParams); @@ -286,7 +325,8 @@ export default async function Home({ searchParams }: HomePageProps) {

{SITE_NAME}

)} - bodyClassName="space-y-6 p-4 md:p-6" + bodyClassName="p-0" > - +
+ + +
+
+
+
+ +
+
+

WYPark HD

+

+ 최근 글 {latestList.length.toLocaleString()}개 · 인기 글 {popularList.length.toLocaleString()}개 +

+
+
+ + {new Intl.DateTimeFormat('ko-KR', { dateStyle: 'medium' }).format(new Date())} + +
+ + + +
+ } + posts={latestList} + featured + /> + } + posts={popularList} + isPopular + /> +
+
+
); diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 69bd001..95cb81f 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { useForm } from 'react-hook-form'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { MailCheck, UserPlus } from 'lucide-react'; import { signup, verifyEmail } from '@/api/auth'; import WindowSurface from '@/components/ui/WindowSurface'; import { SignupRequest } from '@/types'; @@ -74,12 +75,15 @@ export default function SignupPage() { return (
+
+ {step === 'FORM' ? : } +

회원가입

{step === 'FORM' ? '블로그 OS 계정을 만듭니다.' : '이메일로 전송된 6자리 코드를 입력하세요.'} diff --git a/src/components/admin/AdminRouteShell.tsx b/src/components/admin/AdminRouteShell.tsx index 04c5314..50b469a 100644 --- a/src/components/admin/AdminRouteShell.tsx +++ b/src/components/admin/AdminRouteShell.tsx @@ -58,7 +58,7 @@ export default function AdminRouteShell({ children }: { children: React.ReactNod return (

- +

diff --git a/src/components/post/PostListItem.tsx b/src/components/post/PostListItem.tsx index 7a49a43..45a394b 100644 --- a/src/components/post/PostListItem.tsx +++ b/src/components/post/PostListItem.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { ChevronRight, Eye } from 'lucide-react'; +import { ChevronRight, Eye, FileText } from 'lucide-react'; import { clsx } from 'clsx'; import StatusBadge from '@/components/ui/StatusBadge'; import { Post } from '@/types'; @@ -11,7 +11,7 @@ interface PostListItemProps { const isNoticePost = (post: Post) => { const categoryName = post.categoryName || ''; - return categoryName === '공지' || categoryName === '怨듭?' || categoryName.toLowerCase() === 'notice'; + return categoryName === '공지' || categoryName.toLowerCase() === 'notice'; }; const formatDate = (value: string) => { @@ -37,6 +37,9 @@ export default function PostListItem({ post, showViews = false }: PostListItemPr )} >
+ + + {post.categoryName || '미분류'} diff --git a/src/components/ui/Surface.tsx b/src/components/ui/Surface.tsx index e9b71ff..1a32736 100644 --- a/src/components/ui/Surface.tsx +++ b/src/components/ui/Surface.tsx @@ -19,7 +19,7 @@ export default function Surface({ return ( {hasTitlebar && ( -
+
{showTrafficLights && (