블로그 UI 수정
- 리스트와 작성글 3:7 비율로 변경 - 리스트 뷰 10개 넘어가면 pageState 추가 gitblog 블로그 일상 카테고리 글들 이전
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user