홈 중앙 메인 영역을 Thred 간격 기준으로 재구성.

Hero/Featured/Latest 섹션을 내부 컨테이너 기준 보더 정렬로 바꾸고, Latest 목록 카드를 원본 패턴의 리스트 메타 구조로 정리해 중앙 메인 영역의 시각 리듬을 맞췄다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-07 18:09:54 +09:00
parent 4554801294
commit 5e485eb3ec
3 changed files with 162 additions and 70 deletions

View File

@@ -75,7 +75,7 @@
| 파일 | 화면 |
|------|------|
| pages/index.vue | 홈 |
| pages/index.vue | 홈, 중앙 Hero/Featured/Latest 섹션의 내부 컨테이너 보더 정렬과 리스트형 latest 카드 |
| pages/posts/index.vue | 게시물 전체 목록 |
| pages/posts/[slug].vue | `/post/:slug` 리다이렉트 |
| pages/post/[slug].vue | 블로그 글 상세, 게시물 SEO/OG 메타 출력 |

View File

@@ -15,6 +15,7 @@
- 태그 상세 페이지 게시물 메타 영역에 featured 강조, 태그 컬러 배지, 구분자 스타일을 원본 패턴에 맞춰 보정.
- 태그 상세 페이지에서 복수 태그 글은 첫 번째 태그만 배지로 표시하고, 배지와 `/` 구분자가 겹치지 않도록 메타 구조 수정.
- 오른쪽 사이드바 Follow 영역을 원본 패턴의 소셜 아이콘 링크 행으로 교체.
- 홈 중앙 메인 영역을 원본 Thred 구조에 맞춰 Hero/Featured/Latest 섹션 간격과 내부 보더 정렬 기준으로 재구성.
- 기술 명세 현재 버전을 v0.0.45로 갱신.
## v0.0.44

View File

@@ -3,6 +3,10 @@ const { data: posts } = await useFetch('/api/posts', {
default: () => []
})
const { data: tags } = await useFetch('/api/tags', {
default: () => []
})
/**
* 날짜 표시 형식 변환
* @param {string | null} value - ISO 날짜 문자열
@@ -13,90 +17,177 @@ const formatPostDate = (value) => {
return ''
}
const date = new Date(value)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}.${month}.${day}`
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric'
}).format(new Date(value))
}
/**
* 게시물 카드 데이터 변환
* @param {Object} post - API 게시물
* @returns {Object} 게시물 카드 데이터
* 태그 슬러그로 태그 정보 조회
* @param {string | undefined} slug - 태그 슬러그
* @returns {{name: string, color: string}} 태그 정보
*/
const mapPostCard = (post) => ({
title: post.title,
excerpt: post.excerpt,
featuredImage: post.featuredImage,
tag: post.tags?.[0]?.toUpperCase() || 'POST',
publishedAt: formatPostDate(post.publishedAt),
to: `/post/${post.slug}`
})
const getTagMeta = (slug) => {
const matchedTag = tags.value.find((item) => item.slug === slug)
const postCards = computed(() => posts.value.map(mapPostCard))
return {
name: matchedTag?.name || (slug ? slug.toUpperCase() : 'POST'),
color: matchedTag?.color || '#4d4d4d'
}
}
/**
* Latest 목록 데이터 변환
* @param {Object} post - API 게시물
* @param {number} index - 목록 인덱스
* @returns {Object} 화면 표시 데이터
*/
const mapLatestPost = (post, index) => {
const primaryTagSlug = post.tags?.[0]
const tagMeta = getTagMeta(primaryTagSlug)
return {
title: post.title,
excerpt: post.excerpt,
featuredImage: post.featuredImage,
tagName: tagMeta.name,
tagColor: tagMeta.color,
publishedAt: formatPostDate(post.publishedAt),
to: `/post/${post.slug}`,
isFeatured: index === 0
}
}
const featuredPosts = computed(() => posts.value.slice(0, 6))
const latestPosts = computed(() => posts.value.map(mapLatestPost))
</script>
<template>
<MainColumn>
<section class="home-hero site-section">
<div class="home-hero__inner site-section-header text-center">
<h1 class="home-hero__title mx-auto max-w-[620px] text-3xl font-semibold leading-tight tracking-normal md:text-[28px]">
Ideas <em>published</em> for meaningful conversation, discussed and shaped by the community
</h1>
<p class="home-hero__description mx-auto mt-3 max-w-[500px] text-base leading-7 site-muted">
글을 쌓고, 프로젝트와 링크를 연결하고, 오래 쓰기 좋은 개인 블로그를 직접 구축합니다.
</p>
<form class="home-hero__subscribe mx-auto mt-5 flex max-w-[345px] gap-2">
<input class="home-hero__input min-w-0 flex-1 rounded-lg px-3 py-2 text-sm site-input" placeholder="Your email">
<button class="home-hero__button rounded-lg px-5 py-2 text-sm font-semibold site-button" type="button">
Subscribe
</button>
</form>
</div>
</section>
<section class="home-featured site-section">
<div class="home-featured__header site-section-body flex items-center justify-between">
<h2 class="home-featured__title text-sm font-semibold uppercase site-muted">
Featured
</h2>
<div class="home-featured__controls flex gap-4">
<span></span>
<span></span>
<section class="px-5 py-6 sm:px-6 md:py-8">
<div class="mx-auto flex max-w-[720px] flex-col-reverse gap-6">
<div class="z-[2] flex flex-col items-center justify-center gap-2 text-center">
<h1 class="text-xl font-semibold leading-[1.125] md:text-2xl">
Ideas <em>published</em> for meaningful conversation, <em>discussed</em> and shaped by the community
</h1>
<p class="max-w-md text-base leading-snug site-muted">
A modern Ghost theme for curated, community-driven publishing, where members join the conversation.
</p>
<form class="group relative mt-1 flex w-full max-w-xs flex-col items-start">
<fieldset class="flex w-full flex-wrap gap-2 text-sm">
<legend class="sr-only">Personal information</legend>
<input class="site-input flex-[2] rounded-[10px] px-3 py-1.5 text-sm" type="email" placeholder="Your email" aria-label="Your email">
<button class="site-button flex-1 cursor-pointer rounded-[10px] border border-[var(--site-invert)] bg-gradient-to-b from-[rgba(17,17,17,0.75)] to-[rgba(17,17,17,0.95)] px-3 py-1.5 font-medium text-[var(--site-invert-text)] hover:opacity-90" type="button">
Subscribe
</button>
</fieldset>
</form>
</div>
</div>
<div class="home-featured__items grid grid-cols-1 gap-4 px-6 pb-6 md:grid-cols-3">
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#071b22,#0f827c)] p-4 text-white">
<h3 class="mt-20 text-sm font-semibold leading-tight">
Essential tools and techniques for getting started
</h3>
</article>
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#182434,#d4b06b)] p-4 text-white">
<h3 class="mt-20 text-sm font-semibold leading-tight">
Setting up your first home server from scratch
</h3>
</article>
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#141414,#8a5a44)] p-4 text-white">
<h3 class="mt-20 text-sm font-semibold leading-tight">
Writing notes that stay useful over time
</h3>
</article>
</section>
<section class="px-5 py-4 sm:px-6">
<div class="mx-auto max-w-[720px]">
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
<h2 class="text-sm font-medium uppercase site-muted">Featured</h2>
<div class="flex justify-between gap-2">
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Previous"></button>
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Next"></button>
</div>
</div>
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="post in featuredPosts"
:key="`featured-${post.slug}`"
:to="`/post/${post.slug}`"
class="group relative block aspect-video overflow-hidden rounded-[10px]"
>
<img
v-if="post.featuredImage"
:src="post.featuredImage"
:alt="post.title"
class="h-full w-full object-cover brightness-75 contrast-125 transition-all duration-200 group-hover:brightness-90 group-hover:contrast-110"
loading="lazy"
>
<div
v-else
class="h-full w-full bg-[linear-gradient(135deg,#071b22,#5f6f85)]"
/>
<h3 class="absolute right-0 bottom-2.5 left-0 px-3 text-sm font-medium leading-tight text-white line-clamp-2">
{{ post.title }}
</h3>
</NuxtLink>
</div>
</div>
</section>
<section class="home-latest site-section">
<div class="home-latest__header site-section-body flex items-center justify-between">
<h2 class="home-latest__title text-sm font-semibold uppercase site-muted">
Latest
</h2>
<button class="home-latest__view site-interactive rounded-lg px-3 py-2 text-sm site-input" type="button">
목록
</button>
<section class="px-5 py-4 sm:px-6">
<div class="mx-auto max-w-[720px]">
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
<h2 class="text-sm font-medium uppercase site-muted">Latest</h2>
<button class="site-input flex cursor-pointer items-center justify-center gap-1 rounded-[10px] border px-2 py-1.5 text-sm hover:bg-[var(--site-panel)]" type="button">
<span></span>
<span></span>
</button>
</div>
<div class="mb-8 flex flex-col">
<div class="flex flex-col divide-y divide-[var(--site-line)]">
<article
v-for="post in latestPosts"
:key="post.to"
class="group relative flex flex-row gap-3 py-4"
>
<NuxtLink :to="post.to" class="relative aspect-square min-w-16 flex-1 sm:aspect-video">
<figure class="overflow-hidden rounded-[10px]">
<img
v-if="post.featuredImage"
:src="post.featuredImage"
:alt="post.title"
class="aspect-square w-full rounded-[inherit] object-cover transition-opacity duration-200 group-hover:opacity-90 sm:aspect-video"
loading="lazy"
>
<div
v-else
class="aspect-square w-full rounded-[inherit] bg-[linear-gradient(135deg,#253444,#8f9dad)] sm:aspect-video"
/>
</figure>
</NuxtLink>
<div class="relative flex-[3] md:flex-[4]">
<div class="flex h-full flex-col gap-1.5">
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
<NuxtLink :to="post.to" class="transition-opacity duration-200 hover:opacity-75">
<span v-if="post.isFeatured" class="mr-1 inline-flex text-[var(--site-accent)]"></span>
{{ post.title }}
</NuxtLink>
</h2>
<p class="line-clamp-2 flex-1 text-[0.8rem] leading-tight site-muted">
{{ post.excerpt }}
</p>
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
<time>{{ post.publishedAt }}</time>
<span class="text-[var(--site-line)]">/</span>
<span
class="rounded-sm px-1.5 py-px font-medium text-[var(--site-text)]"
:style="{ backgroundColor: `${post.tagColor}1a` }"
>
{{ post.tagName }}
</span>
<span class="text-[var(--site-line)]">/</span>
<span class="flex items-center gap-0.5">
<span></span>
<span>0</span>
</span>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</section>
<PostCard v-for="post in postCards" :key="post.to" :post="post" />
</MainColumn>
</template>