사용자 화면 사이드바 스타일을 Thred 기준으로 정렬.
좌측 네비게이션과 카테고리의 간격 및 hover 인터랙션을 원본 패턴에 맞게 보정하고, 테마 전환/사이드바 전환 애니메이션과 샘플 폴더 Git 제외 설정을 함께 반영해 사용자 화면 일관성을 높였다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,3 +28,7 @@ npm-debug.log*
|
|||||||
|
|
||||||
# Test
|
# Test
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# Reference theme sample (do not commit)
|
||||||
|
ZCF-v1.0.5/
|
||||||
|
sample 깃에 올리지말것/
|
||||||
|
|||||||
@@ -4,22 +4,37 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--site-bg: #fbfbfa;
|
--site-bg: #fcfcfc;
|
||||||
--site-panel: #f6f6f5;
|
--site-panel: #fcfcfc;
|
||||||
--site-panel-strong: #ffffff;
|
--site-panel-strong: #fcfcfc;
|
||||||
--site-text: #111111;
|
--site-text: #111111;
|
||||||
--site-muted: #454545;
|
--site-muted: #454545;
|
||||||
--site-soft: #6f7480;
|
--site-soft: #6f7480;
|
||||||
--site-line: #e2e2e0;
|
--site-line: #e2e2e0;
|
||||||
--site-input: #f2f2f1;
|
--site-input: #fcfcfc;
|
||||||
--site-accent: #ff4f2e;
|
--site-accent: #ff4f2e;
|
||||||
--site-accent-text: #ffffff;
|
--site-accent-text: #ffffff;
|
||||||
--site-invert: #111111;
|
--site-invert: #111111;
|
||||||
--site-invert-text: #ffffff;
|
--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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root:not([data-theme='light']) {
|
||||||
--site-bg: #050505;
|
--site-bg: #050505;
|
||||||
--site-panel: #080808;
|
--site-panel: #080808;
|
||||||
--site-panel-strong: #0d0d0d;
|
--site-panel-strong: #0d0d0d;
|
||||||
@@ -62,17 +77,6 @@
|
|||||||
background: var(--site-bg);
|
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 {
|
.site-section {
|
||||||
border-bottom: 1px solid var(--site-line);
|
border-bottom: 1px solid var(--site-line);
|
||||||
background: var(--site-bg);
|
background: var(--site-bg);
|
||||||
@@ -131,15 +135,48 @@
|
|||||||
border: 1px solid var(--site-line);
|
border: 1px solid var(--site-line);
|
||||||
background: var(--site-input);
|
background: var(--site-input);
|
||||||
color: var(--site-text);
|
color: var(--site-text);
|
||||||
|
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-button {
|
.site-button {
|
||||||
background: var(--site-invert);
|
background: var(--site-invert);
|
||||||
color: var(--site-invert-text);
|
color: var(--site-invert-text);
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-accent-button {
|
.site-accent-button {
|
||||||
background: var(--site-accent);
|
background: var(--site-accent);
|
||||||
color: var(--site-accent-text);
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
menuOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isDarkMode, toggleTheme } = useThemeMode()
|
||||||
|
|
||||||
const { data: tags } = await useFetch('/api/tags', {
|
const { data: tags } = await useFetch('/api/tags', {
|
||||||
default: () => []
|
default: () => []
|
||||||
})
|
})
|
||||||
@@ -12,40 +21,55 @@ const { data: navigation } = await useFetch('/api/navigation', {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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__scroll min-h-0 flex-1 overflow-y-auto">
|
||||||
<div class="left-sidebar__block site-sidebar-section py-3 pl-0 pr-3">
|
<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 grid gap-1 text-[15px]">
|
<nav class="left-sidebar__nav" data-nav="menu">
|
||||||
<NuxtLink
|
<ul class="flex flex-col gap-[3px] text-[15px] text-[var(--site-text)]">
|
||||||
v-for="item in navigation.primary"
|
<li
|
||||||
:key="item.id"
|
v-for="item in navigation.primary"
|
||||||
class="left-sidebar__nav-link py-2 pl-3"
|
:key="item.id"
|
||||||
:to="item.url"
|
class="group relative flex w-full items-center"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<NuxtLink
|
||||||
</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>
|
</nav>
|
||||||
</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-4 pr-3 xl:pl-0">
|
||||||
<div class="left-sidebar__section-title flex items-center justify-between text-xs font-semibold uppercase site-muted">
|
<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>Categories</span>
|
||||||
<span>⌃</span>
|
<span class="text-sm">⌃</span>
|
||||||
</div>
|
</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
|
<NuxtLink
|
||||||
v-for="tag in tags"
|
v-for="tag in tags"
|
||||||
:key="tag.id"
|
: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}`"
|
: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-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">{{ tag.name }}</span>
|
<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>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="left-sidebar__section-title flex items-center justify-between text-xs font-semibold uppercase site-muted">
|
||||||
<span>Authors</span>
|
<span>Authors</span>
|
||||||
<span>⌃</span>
|
<span>⌃</span>
|
||||||
@@ -68,12 +92,22 @@ const { data: navigation } = await useFetch('/api/navigation', {
|
|||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="item in navigation.footer"
|
v-for="item in navigation.footer"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
class="site-interactive"
|
||||||
:to="item.url"
|
:to="item.url"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</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>
|
</footer>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="post-card__body site-section-body flex gap-4">
|
||||||
<img
|
<img
|
||||||
v-if="post.featuredImage"
|
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 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">
|
<div class="post-card__content min-w-0">
|
||||||
<h2 class="post-card__title text-base font-semibold leading-tight">
|
<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 }}
|
{{ post.title }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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__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__block site-sidebar-section py-5 pl-5 pr-0">
|
||||||
<div class="right-sidebar__profile flex items-center gap-3">
|
<div class="right-sidebar__profile flex items-center gap-3">
|
||||||
@@ -55,13 +55,13 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
|||||||
<span>↗</span>
|
<span>↗</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-sidebar__links mt-4 grid gap-3 text-sm">
|
<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 첫 글과 방향
|
sori.studio 첫 글과 방향
|
||||||
</NuxtLink>
|
</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
|
Projects and services
|
||||||
</NuxtLink>
|
</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
|
Links and portal
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const { menuOpen, toggleMenu } = useMenuState()
|
const { menuOpen, toggleMenu } = useMenuState()
|
||||||
|
const { isDarkMode, toggleTheme } = useThemeMode()
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||||
default: () => ({
|
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">
|
<NuxtLink class="site-header__buy site-accent-button rounded-lg px-4 py-2 font-semibold" to="/pages/about">
|
||||||
Subscribe
|
Subscribe
|
||||||
</NuxtLink>
|
</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
|
Account
|
||||||
</NuxtLink>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<section class="tag-header site-section">
|
<section class="tag-header site-section">
|
||||||
<div class="tag-header__inner site-section-header">
|
<div class="tag-header__inner site-section-header">
|
||||||
<p class="tag-header__eyebrow text-xs font-semibold uppercase text-muted">
|
<h1 class="tag-header__title mt-3 text-xl font-semibold leading-tight">
|
||||||
Tag
|
|
||||||
</p>
|
|
||||||
<h1 class="tag-header__title mt-3 text-4xl font-semibold leading-tight">
|
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
<p v-if="description" class="tag-header__description mt-3 text-sm leading-6 text-muted">
|
<p v-if="description" class="tag-header__description mt-3 text-sm leading-6 text-muted">
|
||||||
|
|||||||
66
composables/useThemeMode.js
Normal file
66
composables/useThemeMode.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
## 2026-05-07 v0.0.43
|
||||||
|
|
||||||
### 대표 이미지 액션과 선택 확정 흐름 결정
|
### 대표 이미지 액션과 선택 확정 흐름 결정
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
| 파일 | 화면 위치 |
|
| 파일 | 화면 위치 |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| components/site/SiteHeader.vue | 모든 공개 페이지 상단 |
|
| components/site/SiteHeader.vue | 모든 공개 페이지 상단 |
|
||||||
| components/site/LeftSidebar.vue | 메인 화면 왼쪽, 네비게이션과 태그 목록 |
|
| components/site/LeftSidebar.vue | 메인 화면 왼쪽, 네비게이션과 태그 목록, 테마 전환 버튼, 열림/닫힘 폭 전환 애니메이션, 세로 바→원형 hover 표시 |
|
||||||
| components/site/RightSidebar.vue | 메인 화면 오른쪽, 사이트 설정 표시 |
|
| components/site/RightSidebar.vue | 메인 화면 오른쪽, 사이트 설정 표시, 좌측 경계 보더, 링크 hover 인터랙션 |
|
||||||
| components/site/MainColumn.vue | 메인 화면 중앙 |
|
| components/site/MainColumn.vue | 메인 화면 중앙 |
|
||||||
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일 |
|
| components/site/PostCard.vue | 목록의 게시물 카드, 대표 이미지 썸네일, 카드 hover 인터랙션 |
|
||||||
| components/site/TagHeader.vue | 태그 페이지 헤더 |
|
| components/site/TagHeader.vue | 태그 페이지 헤더 |
|
||||||
|
|
||||||
## 관리자 컴포넌트
|
## 관리자 컴포넌트
|
||||||
@@ -158,8 +158,9 @@
|
|||||||
| package.json | Nuxt 실행 스크립트와 의존성 |
|
| package.json | Nuxt 실행 스크립트와 의존성 |
|
||||||
| nuxt.config.js | Nuxt 앱 설정, Tailwind 모듈 연결, 관리자 QA를 위한 개발 도구 비활성화 |
|
| nuxt.config.js | Nuxt 앱 설정, Tailwind 모듈 연결, 관리자 QA를 위한 개발 도구 비활성화 |
|
||||||
| tailwind.config.js | Tailwind 테마 설정 |
|
| tailwind.config.js | Tailwind 테마 설정 |
|
||||||
| assets/css/main.css | 전역 스타일 |
|
| assets/css/main.css | 전역 스타일, `#fcfcfc` 단일 배경 기준, 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과 |
|
||||||
| composables/useMenuState.js | 좌측 메뉴 열림 상태 관리 |
|
| composables/useMenuState.js | 좌측 메뉴 열림 상태 관리 |
|
||||||
|
| composables/useThemeMode.js | 사용자 화면 라이트/다크 테마 상태 관리 |
|
||||||
| middleware/admin-auth.global.js | 관리자 페이지 접근 인증 |
|
| middleware/admin-auth.global.js | 관리자 페이지 접근 인증 |
|
||||||
| scripts/dev-server.js | 로컬 개발 서버 링크 요약 출력 |
|
| scripts/dev-server.js | 로컬 개발 서버 링크 요약 출력 |
|
||||||
| scripts/migrate-development-db.js | 로컬 개발 DB 마이그레이션 실행 |
|
| scripts/migrate-development-db.js | 로컬 개발 DB 마이그레이션 실행 |
|
||||||
|
|||||||
@@ -27,13 +27,15 @@
|
|||||||
- 헤더 좌측 아이콘은 브랜드 마크가 아니라 왼쪽 사이드바 열기/닫기 버튼
|
- 헤더 좌측 아이콘은 브랜드 마크가 아니라 왼쪽 사이드바 열기/닫기 버튼
|
||||||
- 메뉴 상태는 Nuxt/Vue 상태로 관리
|
- 메뉴 상태는 Nuxt/Vue 상태로 관리
|
||||||
- 브라우저에서는 `localStorage.MENU_STATE`에 `open` 또는 `closed` 저장
|
- 브라우저에서는 `localStorage.MENU_STATE`에 `open` 또는 `closed` 저장
|
||||||
- 닫힘 상태에서는 왼쪽 사이드바를 숨기고 중앙/오른쪽 컬럼만 표시
|
- 닫힘 상태에서는 왼쪽 사이드바 폭을 0으로 줄이는 전환 애니메이션을 적용
|
||||||
|
|
||||||
### 공개 화면 색상
|
### 공개 화면 색상
|
||||||
|
|
||||||
- 라이트/다크 모드는 CSS 변수로 관리
|
- 라이트/다크 모드는 CSS 변수로 관리
|
||||||
- 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리
|
- 기본 배경, 패널, 라인, 텍스트, 보조 텍스트, 입력, 강조 버튼 색상을 분리
|
||||||
|
- 라이트 모드 기본 배경은 `#fcfcfc`로 통일하고 패널 구분은 보더로 처리
|
||||||
- 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응
|
- 시스템 다크 모드는 `prefers-color-scheme: dark` 기준으로 우선 대응
|
||||||
|
- 사용자 수동 테마 전환은 `html[data-theme]`와 `localStorage.SITE_THEME`로 관리
|
||||||
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
|
- Thred 참고 화면처럼 사이드바와 본문은 같은 화면 안에서 구분되는 패널과 라인으로 표현
|
||||||
|
|
||||||
### Post 페이지
|
### Post 페이지
|
||||||
@@ -456,6 +458,6 @@ APP_PORT=43118
|
|||||||
|
|
||||||
## 버전 관리
|
## 버전 관리
|
||||||
|
|
||||||
- 현재 버전: v0.0.43
|
- 현재 버전: v0.0.45
|
||||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정
|
- [ ] 공개 화면 모바일 사이드바/네비게이션 방식 결정
|
||||||
- [ ] Thred 참고 화면 기준 시각 QA
|
- [ ] Thred 참고 화면 기준 시각 QA
|
||||||
- [ ] 사이드바 토글 애니메이션 세부 조정
|
- [ ] 사이드바 토글 애니메이션 세부 조정
|
||||||
|
- [ ] 사용자 화면 테마 전환 초기 로드 깜빡임(FART) 최소화
|
||||||
|
|
||||||
## 콘텐츠 스타일 구현
|
## 콘텐츠 스타일 구현
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
## v0.0.43
|
||||||
|
|
||||||
- 대표 이미지가 설정된 상태의 변경/삭제 액션을 이미지 아래 버튼 영역이 아니라 이미지 hover 오버레이로 수정.
|
- 대표 이미지가 설정된 상태의 변경/삭제 액션을 이미지 아래 버튼 영역이 아니라 이미지 hover 오버레이로 수정.
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ const { menuOpen } = useMenuState()
|
|||||||
<template>
|
<template>
|
||||||
<div class="site-shell public-layout">
|
<div class="site-shell public-layout">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<div class="site-content-grid public-layout__grid" :class="{ 'site-content-grid--menu-closed': !menuOpen }">
|
<div
|
||||||
<LeftSidebar v-show="menuOpen" />
|
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 }">
|
<main class="site-main w-full lg:w-[720px]" :class="{ 'site-main--menu-closed': !menuOpen }">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const postCards = computed(() => posts.value.map(mapPostCard))
|
|||||||
<h2 class="home-latest__title text-sm font-semibold uppercase site-muted">
|
<h2 class="home-latest__title text-sm font-semibold uppercase site-muted">
|
||||||
Latest
|
Latest
|
||||||
</h2>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user