블로그 UI 수정

- 리스트와 작성글 3:7 비율로 변경
 - 리스트 뷰 10개 넘어가면 pageState 추가

gitblog 블로그 일상 카테고리 글들 이전
This commit is contained in:
2026-01-18 13:30:07 +09:00
parent 8462557ee3
commit 370cd4fca0
14 changed files with 839 additions and 21 deletions

View File

@@ -1,24 +1,64 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { getBlogPosts } from '../../data/blog';
import './Blog.css';
const renderInline = (text) => {
const pattern = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
const parts = text.split(pattern).filter(Boolean);
const normalized = text.replace(/<br\s*\/?>/gi, '\n');
const pattern =
/(!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
const segments = normalized.split('\n');
return parts.map((part, index) => {
if (part.startsWith('**')) {
return (
<strong key={`${part}-${index}`}>{part.replace(/\*\*/g, '')}</strong>
);
return segments.flatMap((segment, segmentIndex) => {
const parts = segment.split(pattern).filter(Boolean);
const rendered = parts.map((part, index) => {
if (part.startsWith('![')) {
const match = part.match(/!\[([^\]]*)\]\(([^)]+)\)/);
if (!match) return <span key={`${part}-${index}`}>{part}</span>;
const [, alt, src] = match;
return (
<img
key={`${part}-${index}`}
src={src}
alt={alt}
className="md-image"
loading="lazy"
/>
);
}
if (part.startsWith('[')) {
const match = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (!match) return <span key={`${part}-${index}`}>{part}</span>;
const [, label, href] = match;
return (
<a
key={`${part}-${index}`}
href={href}
target="_blank"
rel="noreferrer"
>
{label}
</a>
);
}
if (part.startsWith('**')) {
return (
<strong key={`${part}-${index}`}>{part.replace(/\*\*/g, '')}</strong>
);
}
if (part.startsWith('*')) {
return <em key={`${part}-${index}`}>{part.replace(/\*/g, '')}</em>;
}
if (part.startsWith('`')) {
return <code key={`${part}-${index}`}>{part.replace(/`/g, '')}</code>;
}
return <span key={`${part}-${index}`}>{part}</span>;
});
if (segmentIndex < segments.length - 1) {
rendered.push(<br key={`br-${segmentIndex}`} />);
}
if (part.startsWith('*')) {
return <em key={`${part}-${index}`}>{part.replace(/\*/g, '')}</em>;
}
if (part.startsWith('`')) {
return <code key={`${part}-${index}`}>{part.replace(/`/g, '')}</code>;
}
return <span key={`${part}-${index}`}>{part}</span>;
return rendered;
});
};
@@ -60,6 +100,18 @@ const renderMarkdown = (body) => {
return;
}
if (/^---$/.test(line.trim())) {
flushList();
blocks.push({ type: 'hr' });
return;
}
if (/^>\s+/.test(line)) {
flushList();
blocks.push({ type: 'quote', value: line.replace(/^>\s+/, '') });
return;
}
if (/^[-*]\s+/.test(line)) {
list.push(line.replace(/^[-*]\s+/, ''));
return;
@@ -108,6 +160,13 @@ const renderMarkdown = (body) => {
<code>{block.value}</code>
</pre>
);
if (block.type === 'quote')
return (
<blockquote key={index} className="md-quote">
{renderInline(block.value)}
</blockquote>
);
if (block.type === 'hr') return <hr key={index} className="md-hr" />;
return (
<p key={index} className="md-paragraph">
{renderInline(block.value)}
@@ -118,8 +177,59 @@ const renderMarkdown = (body) => {
const Blog = () => {
const posts = useMemo(() => getBlogPosts(), []);
const [activeSlug, setActiveSlug] = useState(posts[0]?.slug);
const activePost = posts.find((post) => post.slug === activeSlug) || posts[0];
const categoryNames = ['일상', '개발', '공부', '아이디어'];
const categorized = useMemo(() => {
const map = new Map(categoryNames.map((name) => [name, []]));
const misc = [];
posts.forEach((post) => {
const matched = categoryNames.find((name) => post.tags.includes(name));
if (matched) {
map.get(matched).push(post);
} else {
misc.push(post);
}
});
return {
categories: categoryNames.map((name) => ({
name,
items: map.get(name),
})),
misc,
};
}, [posts]);
const [selectedCategory, setSelectedCategory] = useState('전체');
const [page, setPage] = useState(1);
const pageSize = 10;
const filteredPosts = useMemo(() => {
if (selectedCategory === '전체') return posts;
if (selectedCategory === '기타') return categorized.misc;
return posts.filter((post) => post.tags.includes(selectedCategory));
}, [posts, categorized.misc, selectedCategory]);
const totalPages = Math.max(1, Math.ceil(filteredPosts.length / pageSize));
const pagedPosts = filteredPosts.slice((page - 1) * pageSize, page * pageSize);
const [activeSlug, setActiveSlug] = useState(pagedPosts[0]?.slug);
const activePost =
pagedPosts.find((post) => post.slug === activeSlug) || pagedPosts[0];
useEffect(() => {
if (page > totalPages) {
setPage(1);
}
}, [page, totalPages]);
useEffect(() => {
if (!pagedPosts.find((post) => post.slug === activeSlug)) {
setActiveSlug(pagedPosts[0]?.slug);
}
}, [pagedPosts, activeSlug]);
useEffect(() => {
setPage(1);
}, [selectedCategory]);
return (
<div className="blog">
@@ -141,7 +251,21 @@ const Blog = () => {
<div className="blog-grid">
<aside className="blog-list">
{posts.map((post) => (
<div className="blog-category-filter">
{['전체', ...categoryNames, '기타'].map((name) => (
<button
key={name}
type="button"
className={`blog-category-chip${
selectedCategory === name ? ' is-active' : ''
}`}
onClick={() => setSelectedCategory(name)}
>
{name}
</button>
))}
</div>
{pagedPosts.map((post) => (
<button
key={post.slug}
type="button"
@@ -155,6 +279,29 @@ const Blog = () => {
<span className="blog-list__meta">{post.date || '작성일 미정'}</span>
</button>
))}
<div className="blog-pagination">
<button
type="button"
className="blog-page-btn"
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
>
이전
</button>
<span className="blog-page-indicator">
{page} / {totalPages}
</span>
<button
type="button"
className="blog-page-btn"
onClick={() =>
setPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={page === totalPages}
>
다음
</button>
</div>
</aside>
<article className="blog-article">
{activePost ? (
@@ -183,6 +330,64 @@ const Blog = () => {
)}
</article>
</div>
<section className="blog-categories">
<div className="blog-categories__head">
<h2>카테고리</h2>
<p>태그 기준으로 글을 묶어 한눈에 확인할 있습니다.</p>
</div>
<div className="blog-categories__grid">
{categorized.categories.map((group) => (
<button
key={group.name}
type="button"
className="blog-category-card"
onClick={() => setSelectedCategory(group.name)}
>
<div className="blog-category-card__head">
<span>{group.name}</span>
<span className="blog-category-card__count">
{group.items.length}
</span>
</div>
<div className="blog-category-card__list">
{group.items.length ? (
group.items.slice(0, 3).map((post) => (
<span key={post.slug}>{post.title}</span>
))
) : (
<span className="blog-category-card__empty">
아직 글이 없습니다.
</span>
)}
</div>
</button>
))}
<button
type="button"
className="blog-category-card"
onClick={() => setSelectedCategory('기타')}
>
<div className="blog-category-card__head">
<span>기타</span>
<span className="blog-category-card__count">
{categorized.misc.length}
</span>
</div>
<div className="blog-category-card__list">
{categorized.misc.length ? (
categorized.misc.slice(0, 3).map((post) => (
<span key={post.slug}>{post.title}</span>
))
) : (
<span className="blog-category-card__empty">
아직 글이 없습니다.
</span>
)}
</div>
</button>
</div>
</section>
</div>
);
};