feat(blog): 모바일 반응형 — FAB + 풀다운 리프레시 + 칩 필터

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:52:36 +09:00
parent d5ef77ad17
commit 2b826ed700
4 changed files with 58 additions and 5 deletions

View File

@@ -125,6 +125,18 @@
.bm-empty { text-align: center; padding: 48px 20px; color: rgba(255,255,255,.25); font-size: 0.85rem; } .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) { @media (max-width: 480px) {
.bm { padding: 16px 10px 60px; } .bm { padding: 16px 10px 60px; }
.bm-header h1 { font-size: 1.2rem; } .bm-header h1 { font-size: 1.2rem; }

View File

@@ -1,4 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useIsMobile } from '../../hooks/useIsMobile';
import PullToRefresh from '../../components/PullToRefresh';
import FAB from '../../components/FAB';
import { import {
getBlogMarketingStatus, getBlogMarketingStatus,
startResearch, startResearch,
@@ -81,13 +84,18 @@ function usePollTask(onDone) {
/* ══════════════════════════════════════════════════════════════════════════ */ /* ══════════════════════════════════════════════════════════════════════════ */
export default function BlogMarketing() { export default function BlogMarketing() {
const isMobile = useIsMobile();
const [tab, setTab] = useState('dashboard'); const [tab, setTab] = useState('dashboard');
const [status, setStatus] = useState(null); const [status, setStatus] = useState(null);
useEffect(() => { const loadStatus = useCallback(() => {
getBlogMarketingStatus().then(setStatus).catch(() => {}); return getBlogMarketingStatus().then(setStatus).catch(() => {});
}, []); }, []);
useEffect(() => {
loadStatus();
}, [loadStatus]);
const tabs = [ const tabs = [
{ id: 'dashboard', label: 'Dashboard' }, { id: 'dashboard', label: 'Dashboard' },
{ id: 'research', label: 'Research' }, { id: 'research', label: 'Research' },
@@ -96,6 +104,7 @@ export default function BlogMarketing() {
]; ];
return ( return (
<PullToRefresh onRefresh={loadStatus}>
<div className="bm"> <div className="bm">
<header className="bm-header"> <header className="bm-header">
<h1>Blog Lab</h1> <h1>Blog Lab</h1>
@@ -127,7 +136,12 @@ export default function BlogMarketing() {
{tab === 'research' && <ResearchTab onGenerate={(id) => { setTab('write'); }} />} {tab === 'research' && <ResearchTab onGenerate={(id) => { setTab('write'); }} />}
{tab === 'write' && <WriteTab />} {tab === 'write' && <WriteTab />}
{tab === 'posts' && <PostsTab />} {tab === 'posts' && <PostsTab />}
{tab === 'research' && (
<FAB onClick={() => setTab('research')} label="키워드 분석" />
)}
</div> </div>
</PullToRefresh>
); );
} }

View File

@@ -758,4 +758,19 @@
align-self: stretch; align-self: stretch;
text-align: center; 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;
}
} }

View File

@@ -6,6 +6,9 @@ import {
updateBlogPost, updateBlogPost,
deleteBlogPost, deleteBlogPost,
} from '../../api'; } from '../../api';
import { useIsMobile } from '../../hooks/useIsMobile';
import PullToRefresh from '../../components/PullToRefresh';
import FAB from '../../components/FAB';
import './Blog.css'; import './Blog.css';
// ── 마크다운 렌더러 ────────────────────────────────────────────────────────── // ── 마크다운 렌더러 ──────────────────────────────────────────────────────────
@@ -353,15 +356,15 @@ const BlogEditor = ({ post, onSave, onClose }) => {
// ── 메인 Blog 컴포넌트 ─────────────────────────────────────────────────────── // ── 메인 Blog 컴포넌트 ───────────────────────────────────────────────────────
const Blog = () => { const Blog = () => {
const isMobile = useIsMobile();
const staticPosts = useMemo(() => getBlogPosts(), []); const staticPosts = useMemo(() => getBlogPosts(), []);
const [apiPosts, setApiPosts] = useState([]); const [apiPosts, setApiPosts] = useState([]);
const [apiError, setApiError] = useState(false); const [apiError, setApiError] = useState(false);
const [editorPost, setEditorPost] = useState(null); // null=닫힘, {}=새글, post=수정 const [editorPost, setEditorPost] = useState(null); // null=닫힘, {}=새글, post=수정
const [isEditorOpen, setIsEditorOpen] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false);
// API 글 불러오기 const fetchPosts = useCallback(() => {
useEffect(() => { return getBlogPostsApi()
getBlogPostsApi()
.then((data) => { .then((data) => {
const posts = Array.isArray(data) ? data : (data?.posts ?? []); const posts = Array.isArray(data) ? data : (data?.posts ?? []);
setApiPosts(posts.map((p) => ({ ...p, slug: `api-${p.id}` }))); setApiPosts(posts.map((p) => ({ ...p, slug: `api-${p.id}` })));
@@ -369,6 +372,11 @@ const Blog = () => {
.catch(() => setApiError(true)); .catch(() => setApiError(true));
}, []); }, []);
// API 글 불러오기
useEffect(() => {
fetchPosts();
}, [fetchPosts]);
// 정적 + API 글 병합 (API 글이 앞에 표시) // 정적 + API 글 병합 (API 글이 앞에 표시)
const allPosts = useMemo(() => { const allPosts = useMemo(() => {
const combined = [...apiPosts, ...staticPosts]; const combined = [...apiPosts, ...staticPosts];
@@ -450,6 +458,7 @@ const Blog = () => {
const closeEditor = useCallback(() => { setIsEditorOpen(false); setEditorPost(null); }, []); const closeEditor = useCallback(() => { setIsEditorOpen(false); setEditorPost(null); }, []);
return ( return (
<PullToRefresh onRefresh={fetchPosts}>
<div className="blog"> <div className="blog">
<header className="blog-header"> <header className="blog-header">
<div> <div>
@@ -651,7 +660,10 @@ const Blog = () => {
onClose={closeEditor} onClose={closeEditor}
/> />
)} )}
<FAB onClick={openNewEditor} label="글 쓰기" />
</div> </div>
</PullToRefresh>
); );
}; };