사용자 화면 사이드바 스타일을 Thred 기준으로 정렬.

좌측 네비게이션과 카테고리의 간격 및 hover 인터랙션을 원본 패턴에 맞게 보정하고, 테마 전환/사이드바 전환 애니메이션과 샘플 폴더 Git 제외 설정을 함께 반영해 사용자 화면 일관성을 높였다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-07 17:52:18 +09:00
parent 97d2d8ffb3
commit d47134c46d
15 changed files with 252 additions and 56 deletions

4
.gitignore vendored
View File

@@ -28,3 +28,7 @@ npm-debug.log*
# Test
coverage/
# Reference theme sample (do not commit)
ZCF-v1.0.5/
sample 깃에 올리지말것/

View File

@@ -4,22 +4,37 @@
@layer base {
:root {
--site-bg: #fbfbfa;
--site-panel: #f6f6f5;
--site-panel-strong: #ffffff;
--site-bg: #fcfcfc;
--site-panel: #fcfcfc;
--site-panel-strong: #fcfcfc;
--site-text: #111111;
--site-muted: #454545;
--site-soft: #6f7480;
--site-line: #e2e2e0;
--site-input: #f2f2f1;
--site-input: #fcfcfc;
--site-accent: #ff4f2e;
--site-accent-text: #ffffff;
--site-invert: #111111;
--site-invert-text: #ffffff;
}
:root[data-theme='dark'] {
--site-bg: #050505;
--site-panel: #080808;
--site-panel-strong: #0d0d0d;
--site-text: #f4f4f2;
--site-muted: #c7c7c2;
--site-soft: #8b8e96;
--site-line: #252525;
--site-input: #171717;
--site-accent: #ff4f2e;
--site-accent-text: #ffffff;
--site-invert: #f4f4f2;
--site-invert-text: #111111;
}
@media (prefers-color-scheme: dark) {
:root {
:root:not([data-theme='light']) {
--site-bg: #050505;
--site-panel: #080808;
--site-panel-strong: #0d0d0d;
@@ -62,17 +77,6 @@
background: var(--site-bg);
}
.site-content-grid {
@apply mx-auto grid max-w-[1294px] grid-cols-1 px-4 lg:grid-cols-[287px_minmax(0,720px)_287px] lg:px-0;
min-height: calc(100vh - 57px);
background: var(--site-bg);
}
.site-content-grid--menu-closed {
@apply lg:grid-cols-[minmax(0,720px)_287px];
max-width: 1007px;
}
.site-section {
border-bottom: 1px solid var(--site-line);
background: var(--site-bg);
@@ -131,15 +135,48 @@
border: 1px solid var(--site-line);
background: var(--site-input);
color: var(--site-text);
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
.site-button {
background: var(--site-invert);
color: var(--site-invert-text);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.site-button:hover {
opacity: 0.9;
}
.site-accent-button {
background: var(--site-accent);
color: var(--site-accent-text);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.site-accent-button:hover {
opacity: 0.92;
}
.site-interactive {
transition: color 0.2s ease, background-color 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
}
.site-interactive:hover {
color: var(--site-text);
}
.site-input:hover,
.site-input:focus-visible {
border-color: color-mix(in srgb, var(--site-text) 24%, var(--site-line));
}
.site-panel-hover {
transition: background-color 0.2s ease;
}
.site-panel-hover:hover {
background: color-mix(in srgb, var(--site-panel) 72%, var(--site-text));
}
}

View File

@@ -1,4 +1,13 @@
<script setup>
defineProps({
menuOpen: {
type: Boolean,
required: true
}
})
const { isDarkMode, toggleTheme } = useThemeMode()
const { data: tags } = await useFetch('/api/tags', {
default: () => []
})
@@ -12,40 +21,55 @@ const { data: navigation } = await useFetch('/api/navigation', {
</script>
<template>
<aside class="left-sidebar site-sidebar hidden w-[287px] lg:flex lg:flex-col">
<aside
class="left-sidebar site-sidebar hidden overflow-hidden border-r border-[var(--site-line)] transition-[width,opacity,border-color] duration-300 ease-out lg:flex lg:flex-col"
:class="menuOpen ? 'w-[287px] opacity-100' : 'w-0 opacity-0 border-transparent'"
>
<div class="left-sidebar__scroll min-h-0 flex-1 overflow-y-auto">
<div class="left-sidebar__block site-sidebar-section py-3 pl-0 pr-3">
<nav class="left-sidebar__nav grid gap-1 text-[15px]">
<NuxtLink
v-for="item in navigation.primary"
:key="item.id"
class="left-sidebar__nav-link py-2 pl-3"
:to="item.url"
>
{{ item.label }}
</NuxtLink>
<div class="left-sidebar__block site-sidebar-section py-3 pl-4 pr-3 sm:pl-5 xl:pl-0">
<nav class="left-sidebar__nav" data-nav="menu">
<ul class="flex flex-col gap-[3px] text-[15px] text-[var(--site-text)]">
<li
v-for="item in navigation.primary"
:key="item.id"
class="group relative flex w-full items-center"
>
<NuxtLink
class="left-sidebar__nav-link flex flex-1 items-center gap-2 rounded-[10px] py-1.5 pr-3 pl-0 leading-tight transition-[padding,background-color] duration-200 before:h-4 before:w-1 before:flex-none before:rounded-sm before:rounded-l-none before:bg-[var(--site-line)] before:transition-[width,height,border-radius,background-color] before:duration-200 hover:bg-[#f2f2f2] hover:px-3 hover:before:h-2 hover:before:w-2 hover:before:rounded-full hover:before:bg-[rgba(17,17,17,0.25)]"
:to="item.url"
>
<span class="left-sidebar__nav-link-label flex-1 transition-transform duration-200 group-hover:translate-x-[3px]">{{ item.label }}</span>
</NuxtLink>
</li>
</ul>
</nav>
</div>
<div class="left-sidebar__block site-sidebar-section py-5 pl-0 pr-3">
<div class="left-sidebar__section-title flex items-center justify-between text-xs font-semibold uppercase site-muted">
<div class="left-sidebar__block site-sidebar-section px-5 py-4 pr-3 xl:pl-0">
<div class="left-sidebar__section-title flex items-center justify-between pr-2 text-xs font-semibold uppercase tracking-[0.01em] site-muted">
<span>Categories</span>
<span></span>
<span class="text-sm"></span>
</div>
<div class="left-sidebar__category-grid mt-4 grid grid-cols-2 gap-x-6 gap-y-4 text-sm">
<div class="left-sidebar__category-grid mt-1.5 grid grid-cols-2 gap-x-2 gap-y-[2px] text-[0.8rem] font-medium">
<NuxtLink
v-for="tag in tags"
:key="tag.id"
class="left-sidebar__category flex items-center gap-3"
class="left-sidebar__category group flex items-center gap-2 rounded-[10px] py-2 pr-3 pl-0 leading-tight transition-[padding,background-color,color] duration-200 hover:bg-[#f2f2f2] hover:px-3"
:to="`/tag/${tag.slug}`"
>
<span class="left-sidebar__category-color h-4 w-1 rounded-full" :style="{ backgroundColor: tag.color }" />
<span class="left-sidebar__category-name">{{ tag.name }}</span>
<span class="left-sidebar__category-color h-4 w-1 rounded-sm rounded-l-none transition-all duration-200 group-hover:h-2 group-hover:w-2 group-hover:rounded-full" :style="{ backgroundColor: tag.color }" />
<span class="left-sidebar__category-name flex-1 truncate transition-transform duration-200 group-hover:translate-x-[3px]">{{ tag.name }}</span>
<span
v-if="tag.postCount"
class="left-sidebar__category-count invisible text-xs font-medium opacity-0 transition-opacity duration-200 group-hover:visible group-hover:opacity-100"
>
{{ tag.postCount }}
</span>
</NuxtLink>
</div>
</div>
<div class="left-sidebar__block site-sidebar-section py-5 pl-0 pr-3">
<div class="left-sidebar__block site-sidebar-section px-5 py-5 pr-3 xl:pl-0">
<div class="left-sidebar__section-title flex items-center justify-between text-xs font-semibold uppercase site-muted">
<span>Authors</span>
<span></span>
@@ -68,12 +92,22 @@ const { data: navigation } = await useFetch('/api/navigation', {
<NuxtLink
v-for="item in navigation.footer"
:key="item.id"
class="site-interactive"
:to="item.url"
>
{{ item.label }}
</NuxtLink>
</nav>
<span class="left-sidebar__theme-dot"></span>
<button
class="left-sidebar__theme-dot site-panel-hover site-interactive grid h-7 w-7 place-items-center rounded-full border border-[var(--site-line)]"
type="button"
:aria-label="isDarkMode ? '라이트 모드로 전환' : '다크 모드로 전환'"
:title="isDarkMode ? '라이트 모드' : '다크 모드'"
@click="toggleTheme"
>
<span v-if="isDarkMode"></span>
<span v-else></span>
</button>
</footer>
</aside>
</template>

View File

@@ -8,7 +8,7 @@ defineProps({
</script>
<template>
<article class="post-card site-section">
<article class="post-card site-section site-panel-hover">
<div class="post-card__body site-section-body flex gap-4">
<img
v-if="post.featuredImage"
@@ -20,7 +20,7 @@ defineProps({
<div v-else class="post-card__thumb h-20 w-36 shrink-0 rounded-lg bg-[linear-gradient(135deg,#06333a,#f4a261)]" />
<div class="post-card__content min-w-0">
<h2 class="post-card__title text-base font-semibold leading-tight">
<NuxtLink class="post-card__title-link hover:opacity-70" :to="post.to">
<NuxtLink class="post-card__title-link site-interactive hover:opacity-70" :to="post.to">
{{ post.title }}
</NuxtLink>
</h2>

View File

@@ -10,7 +10,7 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
</script>
<template>
<aside class="right-sidebar site-sidebar hidden w-[287px] lg:flex lg:flex-col">
<aside class="right-sidebar site-sidebar hidden w-[287px] border-l border-[var(--site-line)] lg:flex lg:flex-col">
<div class="right-sidebar__scroll min-h-0 flex-1 overflow-y-auto">
<div class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0">
<div class="right-sidebar__profile flex items-center gap-3">
@@ -55,13 +55,13 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
<span></span>
</div>
<div class="right-sidebar__links mt-4 grid gap-3 text-sm">
<NuxtLink class="right-sidebar__link font-semibold" to="/post/hello-sori-studio">
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/post/hello-sori-studio">
sori.studio 글과 방향
</NuxtLink>
<NuxtLink class="right-sidebar__link font-semibold" to="/pages/projects">
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/pages/projects">
Projects and services
</NuxtLink>
<NuxtLink class="right-sidebar__link font-semibold" to="/pages/links">
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/pages/links">
Links and portal
</NuxtLink>
</div>

View File

@@ -1,5 +1,6 @@
<script setup>
const { menuOpen, toggleMenu } = useMenuState()
const { isDarkMode, toggleTheme } = useThemeMode()
const { data: siteSettings } = await useFetch('/api/site-settings', {
default: () => ({
@@ -53,9 +54,19 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
<NuxtLink class="site-header__buy site-accent-button rounded-lg px-4 py-2 font-semibold" to="/pages/about">
Subscribe
</NuxtLink>
<NuxtLink class="site-header__nav-link hover:text-ink" to="/pages/about">
<NuxtLink class="site-header__nav-link site-interactive rounded-md px-2 py-1" to="/pages/about">
Account
</NuxtLink>
<button
class="site-header__theme-toggle site-panel-hover site-interactive grid h-8 w-8 place-items-center rounded-full border border-[var(--site-line)]"
type="button"
:aria-label="isDarkMode ? '라이트 모드로 전환' : '다크 모드로 전환'"
:title="isDarkMode ? '라이트 모드' : '다크 모드'"
@click="toggleTheme"
>
<span v-if="isDarkMode"></span>
<span v-else></span>
</button>
</nav>
</div>
</header>

View File

@@ -14,10 +14,7 @@ defineProps({
<template>
<section class="tag-header site-section">
<div class="tag-header__inner site-section-header">
<p class="tag-header__eyebrow text-xs font-semibold uppercase text-muted">
Tag
</p>
<h1 class="tag-header__title mt-3 text-4xl font-semibold leading-tight">
<h1 class="tag-header__title mt-3 text-xl font-semibold leading-tight">
{{ title }}
</h1>
<p v-if="description" class="tag-header__description mt-3 text-sm leading-6 text-muted">

View File

@@ -0,0 +1,66 @@
const themeStorageKey = 'SITE_THEME'
/**
* HTML 루트 요소에 현재 테마를 반영한다.
* @param {'light' | 'dark'} theme - 적용할 테마
* @returns {void}
*/
const applyThemeToDocument = (theme) => {
if (!import.meta.client) {
return
}
document.documentElement.dataset.theme = theme
document.documentElement.style.colorScheme = theme
}
/**
* 사용자의 시스템 테마를 조회한다.
* @returns {'light' | 'dark'} 시스템 기준 기본 테마
*/
const getSystemTheme = () => {
if (!import.meta.client) {
return 'light'
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
/**
* 사이트 라이트/다크 테마 상태를 관리한다.
* @returns {{theme: import('vue').Ref<'light' | 'dark'>, isDarkMode: import('vue').ComputedRef<boolean>, toggleTheme: Function}} 테마 상태와 제어 함수
*/
export const useThemeMode = () => {
const theme = useState('site-theme-mode', () => 'light')
const isDarkMode = computed(() => theme.value === 'dark')
onMounted(() => {
const savedTheme = localStorage.getItem(themeStorageKey)
const nextTheme = savedTheme === 'light' || savedTheme === 'dark' ? savedTheme : getSystemTheme()
theme.value = nextTheme
applyThemeToDocument(nextTheme)
})
watch(theme, (nextTheme) => {
if (!import.meta.client) {
return
}
localStorage.setItem(themeStorageKey, nextTheme)
applyThemeToDocument(nextTheme)
})
/**
* 라이트/다크 테마를 전환한다.
* @returns {void}
*/
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
return {
theme,
isDarkMode,
toggleTheme
}
}

View File

@@ -1,5 +1,25 @@
# 의사결정 이력
## 2026-05-07 v0.0.45
### 사용자 화면 단일 배경과 사이드바 전환 방식 결정
사용자 화면 라이트 모드는 전체 배경을 `#fcfcfc`로 통일하고, 영역 구분은 색상 차이가 아니라 보더로만 처리한다. Thred 참고 화면처럼 배경 톤 편차를 줄이면 카드, 사이드바, 본문이 하나의 캔버스 안에서 정돈되어 보이고 시선이 콘텐츠와 타이포에 더 집중되기 때문이다.
왼쪽 사이드바는 열림/닫힘 시 DOM을 제거하지 않고 너비를 애니메이션으로 줄인다. 사이드바가 즉시 사라지면 레이아웃이 튀어 보이므로, 그리드 컬럼과 사이드바 폭을 함께 전환해 스르륵 접히는 느낌을 유지한다.
왼쪽 네비게이션 항목은 기본 상태에서 회색 세로 바를 보이고 hover/focus 시 원형 아이콘으로 전환한다. 정적 상태에서는 구분선을 제공하고 상호작용 시 클릭 가능 영역을 명확하게 드러내기 위해서다.
## 2026-05-07 v0.0.44
### 사용자 화면 테마 상태 저장과 샘플 폴더 제외 결정
사용자 화면 라이트/다크 모드는 시스템 테마 자동 감지에만 의존하지 않고 수동 전환 상태를 `localStorage.SITE_THEME`에 저장한다. 공개 화면 헤더와 사이드바에서 같은 테마 상태를 공유해야 하며, 다음 방문에서도 사용자가 마지막으로 선택한 테마를 유지해야 하기 때문이다.
테마 색상 적용은 CSS 변수와 `html[data-theme]` 조합으로 처리한다. 기존 `prefers-color-scheme`는 기본 fallback으로 유지하되, 사용자가 명시적으로 라이트를 고른 경우 시스템이 다크여도 의도한 화면이 유지되도록 우선순위를 분리한다.
Thred 참고용 샘플인 `ZCF-v1.0.5`와 보관 폴더는 레퍼런스 자료로만 사용하고 Git 추적 대상에서 제외한다. 대용량 정적 자산과 외부 테마 원본이 변경 이력에 섞이면 실제 서비스 코드 변경 검토가 어려워지기 때문이다.
## 2026-05-07 v0.0.43
### 대표 이미지 액션과 선택 확정 흐름 결정

View File

@@ -16,10 +16,10 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/site/SiteHeader.vue | 모든 공개 페이지 상단 |
| components/site/LeftSidebar.vue | 메인 화면 왼쪽, 네비게이션과 태그 목록 |
| components/site/RightSidebar.vue | 메인 화면 오른쪽, 사이트 설정 표시 |
| components/site/LeftSidebar.vue | 메인 화면 왼쪽, 네비게이션과 태그 목록, 테마 전환 버튼, 열림/닫힘 폭 전환 애니메이션, 세로 바→원형 hover 표시 |
| components/site/RightSidebar.vue | 메인 화면 오른쪽, 사이트 설정 표시, 좌측 경계 보더, 링크 hover 인터랙션 |
| components/site/MainColumn.vue | 메인 화면 중앙 |
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일 |
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일, 카드 hover 인터랙션 |
| components/site/TagHeader.vue | 태그 페이지 헤더 |
## 관리자 컴포넌트
@@ -158,8 +158,9 @@
| package.json | Nuxt 실행 스크립트와 의존성 |
| nuxt.config.js | Nuxt 앱 설정, Tailwind 모듈 연결, 관리자 QA를 위한 개발 도구 비활성화 |
| tailwind.config.js | Tailwind 테마 설정 |
| assets/css/main.css | 전역 스타일 |
| assets/css/main.css | 전역 스타일, `#fcfcfc` 단일 배경 기준, 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과 |
| composables/useMenuState.js | 좌측 메뉴 열림 상태 관리 |
| composables/useThemeMode.js | 사용자 화면 라이트/다크 테마 상태 관리 |
| middleware/admin-auth.global.js | 관리자 페이지 접근 인증 |
| scripts/dev-server.js | 로컬 개발 서버 링크 요약 출력 |
| scripts/migrate-development-db.js | 로컬 개발 DB 마이그레이션 실행 |

View File

@@ -27,13 +27,15 @@
- 헤더 좌측 아이콘은 브랜드 마크가 아니라 왼쪽 사이드바 열기/닫기 버튼
- 메뉴 상태는 Nuxt/Vue 상태로 관리
- 브라우저에서는 `localStorage.MENU_STATE``open` 또는 `closed` 저장
- 닫힘 상태에서는 왼쪽 사이드바를 숨기고 중앙/오른쪽 컬럼만 표시
- 닫힘 상태에서는 왼쪽 사이드바 폭을 0으로 줄이는 전환 애니메이션을 적용
### 공개 화면 색상
- 라이트/다크 모드는 CSS 변수로 관리
- 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리
- 라이트 모드 기본 배경은 `#fcfcfc`로 통일하고 패널 구분은 보더로 처리
- 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응
- 사용자 수동 테마 전환은 `html[data-theme]``localStorage.SITE_THEME`로 관리
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
### Post 페이지
@@ -456,6 +458,6 @@ APP_PORT=43118
## 버전 관리
- 현재 버전: v0.0.43
- 현재 버전: v0.0.45
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정

View File

@@ -13,6 +13,7 @@
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정
- [ ] Thred 참고 화면 기준 시각 QA
- [ ] 사이드바 토글 애니메이션 세부 조정
- [ ] 사용자 화면 테마 전환 초기 로드 깜빡임(FART) 최소화
## 콘텐츠 스타일 구현

View File

@@ -1,5 +1,25 @@
# 업데이트 이력
## v0.0.45
- 사용자 화면 기본 배경을 `#fcfcfc`로 통일하고 보더 기준 구분으로 정리.
- 오른쪽 사이드바 왼쪽 경계선이 항상 보이도록 보더 추가.
- 왼쪽 사이드바 열림/닫힘 시 폭이 스르륵 줄어드는 전환 애니메이션 추가.
- 왼쪽 네비게이션 항목 왼쪽에 회색 세로 바 표시를 추가하고 hover 시 원형 아이콘으로 전환되도록 수정.
- 왼쪽 네비게이션 hover 배경색을 더 연하게 조정하고, 마우스 오버 시 텍스트가 함께 이동하도록 전환 효과 보정.
- 왼쪽 사이드바 전환과 네비게이션 hover 효과 구현을 커스텀 CSS에서 Tailwind 유틸리티 클래스로 전환.
- 왼쪽 네비게이션과 카테고리 영역의 패딩, 간격, hover 동작을 원본 Thred 마크업 패턴에 맞춰 재정렬.
- 왼쪽 사이드바 네비게이션/카테고리/작성자 섹션의 내부 패딩과 텍스트 이동량을 미세 조정.
- 기술 명세 현재 버전을 v0.0.45로 갱신.
## v0.0.44
- 사용자 화면 다크/라이트 테마 전환 composable 추가.
- 헤더와 좌측 사이드바에 테마 전환 버튼 연결.
- 사용자 화면 링크, 카드, 입력, 버튼 hover 인터랙션 보강.
- `ZCF-v1.0.5` 및 샘플 폴더가 Git에 포함되지 않도록 제외 규칙 추가.
- 기술 명세 현재 버전을 v0.0.44로 갱신.
## v0.0.43
- 대표 이미지가 설정된 상태의 변경/삭제 액션을 이미지 아래 버튼 영역이 아니라 이미지 hover 오버레이로 수정.

View File

@@ -5,8 +5,11 @@ const { menuOpen } = useMenuState()
<template>
<div class="site-shell public-layout">
<SiteHeader />
<div class="site-content-grid public-layout__grid" :class="{ 'site-content-grid--menu-closed': !menuOpen }">
<LeftSidebar v-show="menuOpen" />
<div
class="public-layout__grid mx-auto grid min-h-[calc(100vh-57px)] max-w-[1294px] grid-cols-1 bg-[var(--site-bg)] px-4 transition-[grid-template-columns,max-width] duration-300 ease-out lg:px-0 lg:[grid-template-columns:287px_minmax(0,720px)_287px]"
:class="menuOpen ? '' : 'max-w-[1007px] lg:[grid-template-columns:0_minmax(0,720px)_287px]'"
>
<LeftSidebar :menu-open="menuOpen" />
<main class="site-main w-full lg:w-[720px]" :class="{ 'site-main--menu-closed': !menuOpen }">
<slot />
</main>

View File

@@ -91,7 +91,7 @@ const postCards = computed(() => posts.value.map(mapPostCard))
<h2 class="home-latest__title text-sm font-semibold uppercase site-muted">
Latest
</h2>
<button class="home-latest__view rounded-lg px-3 py-2 text-sm site-input" type="button">
<button class="home-latest__view site-interactive rounded-lg px-3 py-2 text-sm site-input" type="button">
목록
</button>
</div>