From 2b826ed7002005dc717fc4dc4025ffb1d615e5d0 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 23 Apr 2026 14:52:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(blog):=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=E2=80=94=20FAB=20+=20=ED=92=80?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20+?= =?UTF-8?q?=20=EC=B9=A9=20=ED=95=84=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/pages/blog-marketing/BlogMarketing.css | 12 ++++++++++++ src/pages/blog-marketing/BlogMarketing.jsx | 18 ++++++++++++++++-- src/pages/blog/Blog.css | 15 +++++++++++++++ src/pages/blog/Blog.jsx | 18 +++++++++++++++--- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/pages/blog-marketing/BlogMarketing.css b/src/pages/blog-marketing/BlogMarketing.css index e597b18..4ac6ae8 100644 --- a/src/pages/blog-marketing/BlogMarketing.css +++ b/src/pages/blog-marketing/BlogMarketing.css @@ -125,6 +125,18 @@ .bm-empty { text-align: center; padding: 48px 20px; color: rgba(255,255,255,.25); font-size: 0.85rem; } /* ── 모바일 ───────────────────────────────────────────────────────────────── */ +@media (max-width: 768px) { + .bm-tabs { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .bm-tabs > * { + flex-shrink: 0; + white-space: nowrap; + } +} + @media (max-width: 480px) { .bm { padding: 16px 10px 60px; } .bm-header h1 { font-size: 1.2rem; } diff --git a/src/pages/blog-marketing/BlogMarketing.jsx b/src/pages/blog-marketing/BlogMarketing.jsx index f602d1c..131661c 100644 --- a/src/pages/blog-marketing/BlogMarketing.jsx +++ b/src/pages/blog-marketing/BlogMarketing.jsx @@ -1,4 +1,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import PullToRefresh from '../../components/PullToRefresh'; +import FAB from '../../components/FAB'; import { getBlogMarketingStatus, startResearch, @@ -81,13 +84,18 @@ function usePollTask(onDone) { /* ══════════════════════════════════════════════════════════════════════════ */ export default function BlogMarketing() { + const isMobile = useIsMobile(); const [tab, setTab] = useState('dashboard'); const [status, setStatus] = useState(null); - useEffect(() => { - getBlogMarketingStatus().then(setStatus).catch(() => {}); + const loadStatus = useCallback(() => { + return getBlogMarketingStatus().then(setStatus).catch(() => {}); }, []); + useEffect(() => { + loadStatus(); + }, [loadStatus]); + const tabs = [ { id: 'dashboard', label: 'Dashboard' }, { id: 'research', label: 'Research' }, @@ -96,6 +104,7 @@ export default function BlogMarketing() { ]; return ( +

Blog Lab

@@ -127,7 +136,12 @@ export default function BlogMarketing() { {tab === 'research' && { setTab('write'); }} />} {tab === 'write' && } {tab === 'posts' && } + + {tab === 'research' && ( + setTab('research')} label="키워드 분석" /> + )}
+
); } diff --git a/src/pages/blog/Blog.css b/src/pages/blog/Blog.css index 119cd59..7978bba 100644 --- a/src/pages/blog/Blog.css +++ b/src/pages/blog/Blog.css @@ -758,4 +758,19 @@ align-self: stretch; text-align: center; } + + /* 태그/카테고리 필터 가로 스크롤 */ + .blog-categories, + .blog-category-list { + display: flex; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + flex-wrap: nowrap; + gap: 8px; + } + + .blog-categories > *, + .blog-category-list > * { + flex-shrink: 0; + } } diff --git a/src/pages/blog/Blog.jsx b/src/pages/blog/Blog.jsx index a23698d..a82bcb4 100644 --- a/src/pages/blog/Blog.jsx +++ b/src/pages/blog/Blog.jsx @@ -6,6 +6,9 @@ import { updateBlogPost, deleteBlogPost, } from '../../api'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import PullToRefresh from '../../components/PullToRefresh'; +import FAB from '../../components/FAB'; import './Blog.css'; // ── 마크다운 렌더러 ────────────────────────────────────────────────────────── @@ -353,15 +356,15 @@ const BlogEditor = ({ post, onSave, onClose }) => { // ── 메인 Blog 컴포넌트 ─────────────────────────────────────────────────────── const Blog = () => { + const isMobile = useIsMobile(); const staticPosts = useMemo(() => getBlogPosts(), []); const [apiPosts, setApiPosts] = useState([]); const [apiError, setApiError] = useState(false); const [editorPost, setEditorPost] = useState(null); // null=닫힘, {}=새글, post=수정 const [isEditorOpen, setIsEditorOpen] = useState(false); - // API 글 불러오기 - useEffect(() => { - getBlogPostsApi() + const fetchPosts = useCallback(() => { + return getBlogPostsApi() .then((data) => { const posts = Array.isArray(data) ? data : (data?.posts ?? []); setApiPosts(posts.map((p) => ({ ...p, slug: `api-${p.id}` }))); @@ -369,6 +372,11 @@ const Blog = () => { .catch(() => setApiError(true)); }, []); + // API 글 불러오기 + useEffect(() => { + fetchPosts(); + }, [fetchPosts]); + // 정적 + API 글 병합 (API 글이 앞에 표시) const allPosts = useMemo(() => { const combined = [...apiPosts, ...staticPosts]; @@ -450,6 +458,7 @@ const Blog = () => { const closeEditor = useCallback(() => { setIsEditorOpen(false); setEditorPost(null); }, []); return ( +
@@ -651,7 +660,10 @@ const Blog = () => { onClose={closeEditor} /> )} + +
+ ); };