{apiError ? '블로그 API에 연결할 수 없습니다. 백엔드 서버를 확인해 주세요.' : '아직 작성된 글이 없습니다. 새 글 쓰기 버튼으로 첫 글을 작성해 보세요.'}
)}import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getBlogPosts } from '../../data/blog';
import {
getBlogPostsApi,
createBlogPost,
updateBlogPost,
deleteBlogPost,
} from '../../api';
import PullToRefresh from '../../components/PullToRefresh';
import FAB from '../../components/FAB';
import './Blog.css';
// ── 마크다운 렌더러 ──────────────────────────────────────────────────────────
const renderInline = (text) => {
const normalized = text.replace(/
/gi, '\n');
const pattern =
/(!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|\*\*[^*]+\*\*|\*[^*]+\*|~~[^~]+~~|`[^`]+`)/g;
const segments = normalized.split('\n');
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 {part};
const [, alt, src] = match;
return (
);
}
if (part.startsWith('[')) {
const match = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (!match) return {part};
const [, label, href] = match;
return (
{label}
);
}
if (part.startsWith('**')) {
return (
{part.replace(/\*\*/g, '')}
);
}
if (part.startsWith('*')) {
return {part.replace(/\*/g, '')};
}
if (part.startsWith('~~')) {
return
{part.replace(/~~/g, '')};
}
if (part.startsWith('`')) {
return {part.replace(/`/g, '')};
}
return {part};
});
if (segmentIndex < segments.length - 1) {
rendered.push(
);
}
return rendered;
});
};
const renderMarkdown = (body) => {
const lines = body.split(/\r?\n/);
const blocks = [];
let list = [];
let code = [];
let inCode = false;
const flushList = () => {
if (list.length) {
blocks.push({ type: 'list', items: list });
list = [];
}
};
const flushCode = () => {
if (code.length) {
blocks.push({ type: 'code', value: code.join('\n') });
code = [];
}
};
lines.forEach((line) => {
if (line.startsWith('```')) {
if (inCode) {
flushCode();
inCode = false;
} else {
flushList();
inCode = true;
}
return;
}
if (inCode) {
code.push(line);
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;
}
flushList();
if (!line.trim()) return;
if (line.startsWith('###### ')) {
blocks.push({ type: 'h6', value: line.replace(/^######\s+/, '') });
return;
}
if (line.startsWith('##### ')) {
blocks.push({ type: 'h5', value: line.replace(/^#####\s+/, '') });
return;
}
if (line.startsWith('#### ')) {
blocks.push({ type: 'h4', value: line.replace(/^####\s+/, '') });
return;
}
if (line.startsWith('### ')) {
blocks.push({ type: 'h3', value: line.replace(/^###\s+/, '') });
return;
}
if (line.startsWith('## ')) {
blocks.push({ type: 'h2', value: line.replace(/^##\s+/, '') });
return;
}
if (line.startsWith('# ')) {
blocks.push({ type: 'h1', value: line.replace(/^#\s+/, '') });
return;
}
blocks.push({ type: 'p', value: line });
});
flushList();
flushCode();
return blocks.map((block, index) => {
if (block.type === 'h1') return
{block.value}
);
if (block.type === 'quote')
return (
{renderInline(block.value)}); if (block.type === 'hr') return
{renderInline(block.value)}
); }); }; // ── 블로그 에디터 모달 ──────────────────────────────────────────────────────── const PRESET_TAGS = ['일상', '개발', '공부', '아이디어', '기타']; const BlogEditor = ({ post, onSave, onClose }) => { const [title, setTitle] = useState(post?.title || ''); const [tags, setTags] = useState(post?.tags || []); const [body, setBody] = useState(post?.body || ''); const [showPreview, setShowPreview] = useState(false); const [saving, setSaving] = useState(false); const textareaRef = useRef(null); // Tab 키로 들여쓰기 삽입 const handleKeyDown = (e) => { if (e.key === 'Tab') { e.preventDefault(); const el = textareaRef.current; const start = el.selectionStart; const end = el.selectionEnd; const next = body.substring(0, start) + ' ' + body.substring(end); setBody(next); requestAnimationFrame(() => { el.selectionStart = el.selectionEnd = start + 2; }); } }; const toggleTag = (tag) => { setTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag] ); }; const handleSave = async () => { if (!title.trim()) return; setSaving(true); try { const today = new Date().toISOString().slice(0, 10); const excerpt = body .split(/\r?\n/) .find((l) => l.trim() && !l.startsWith('#')) ?.trim() .slice(0, 120) || ''; await onSave({ title: title.trim(), tags, body, excerpt, date: post?.date || today, }); onClose(); } finally { setSaving(false); } }; // ESC 키로 닫기 useEffect(() => { const handler = (e) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [onClose]); return (본문을 입력하면 여기에 미리보기가 표시됩니다.
}Journal
글을 작성하고 태그를 달아 정리하세요.
이번 주의 기록
손에 닿는 생각을 즉시 적어두고, 나중에 다시 꺼내어 다듬습니다.
{apiError ? '블로그 API에 연결할 수 없습니다. 백엔드 서버를 확인해 주세요.' : '아직 작성된 글이 없습니다. 새 글 쓰기 버튼으로 첫 글을 작성해 보세요.'}
)}태그 기준으로 글을 묶어 한눈에 확인할 수 있습니다.