feat(blog): 모바일 반응형 — FAB + 풀다운 리프레시 + 칩 필터
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user