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 ( {alt} ); } 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 === 'h2') return

{block.value}

; if (block.type === 'h3') return

{block.value}

; if (block.type === 'h4') return

{block.value}

; if (block.type === 'h5') return
{block.value}
; if (block.type === 'h6') return
{block.value}
; if (block.type === 'list') return ( ); if (block.type === 'code') return (
                    {block.value}
                
); if (block.type === 'quote') return (
{renderInline(block.value)}
); if (block.type === 'hr') return
; 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 (
{ if (e.target === e.currentTarget) onClose(); }}>

{post?.id ? '글 수정' : '새 글 쓰기'}

setTitle(e.target.value)} autoFocus />
{PRESET_TAGS.map((tag) => ( ))}
{showPreview ? (
{body ? renderMarkdown(body) :

본문을 입력하면 여기에 미리보기가 표시됩니다.

}
) : (