wing-ops/frontend/src/tabs/board/components/BoardDetailView.tsx

150 lines
5.3 KiB
TypeScript
Executable File

import { useState, useEffect } from 'react';
import { useAuthStore } from '@common/store/authStore';
import { fetchBoardPost, type BoardPostDetail } from '../services/boardApi';
// 카테고리 코드 → 표시명
const CATEGORY_LABELS: Record<string, string> = {
NOTICE: '공지사항',
DATA: '자료실',
QNA: 'Q&A',
MANUAL: '해경매뉴얼',
};
// 카테고리별 배지 색상 (NOTICE는 danger, 나머지는 중립)
const CATEGORY_COLORS: Record<string, string> = {
NOTICE: 'bg-[color-mix(in_srgb,var(--color-danger)_15%,transparent)] text-color-danger',
DATA: 'bg-bg-elevated text-fg-sub',
QNA: 'bg-bg-elevated text-fg-sub',
MANUAL: 'bg-bg-elevated text-fg-sub',
};
interface BoardDetailViewProps {
postSn: number;
onBack: () => void;
onEdit: () => void;
onDelete: () => void;
}
export function BoardDetailView({ postSn, onBack, onEdit, onDelete }: BoardDetailViewProps) {
const user = useAuthStore((s) => s.user);
const [post, setPost] = useState<BoardPostDetail | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const load = async () => {
setIsLoading(true);
try {
const data = await fetchBoardPost(postSn);
if (!cancelled) setPost(data);
} catch {
if (!cancelled) {
alert('게시글을 불러오는데 실패했습니다.');
onBack();
}
} finally {
if (!cancelled) setIsLoading(false);
}
};
load();
return () => {
cancelled = true;
};
}, [postSn, onBack]);
if (isLoading || !post) {
return (
<div className="flex flex-col h-full bg-bg-base items-center justify-center">
<p className="text-fg-disabled text-label-1"> ...</p>
</div>
);
}
// 본인 게시글 여부
const isAuthor = user?.id === post.authorId;
return (
<div className="flex flex-col h-full bg-bg-base">
{/* 헤더 */}
<div className="flex items-center justify-between px-8 py-4 border-b border-stroke bg-bg-surface">
<button
onClick={onBack}
className="flex items-center gap-2 text-label-1 font-semibold text-fg-sub hover:text-fg transition-colors"
>
<span></span>
<span></span>
</button>
{isAuthor && (
<div className="flex items-center gap-2">
<button
onClick={onEdit}
className="px-4 py-2 text-label-1 font-semibold rounded text-fg bg-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] border border-[color-mix(in_srgb,var(--color-accent)_30%,transparent)] cursor-pointer"
>
</button>
<button
onClick={onDelete}
className="px-4 py-2 text-label-1 font-semibold rounded text-fg bg-[color-mix(in_srgb,var(--color-danger)_30%,transparent)] border border-[color-mix(in_srgb,var(--color-danger)_30%,transparent)] cursor-pointer"
>
</button>
</div>
)}
</div>
{/* 게시글 내용 */}
<div className="flex-1 overflow-auto">
<div className="max-w-4xl mx-auto px-8 py-8">
{/* 게시글 헤더 */}
<div className="pb-6 border-b border-stroke">
<div className="flex items-center gap-3 mb-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded text-caption font-semibold ${CATEGORY_COLORS[post.categoryCd] || 'bg-bg-elevated text-fg-sub'}`}
>
{CATEGORY_LABELS[post.categoryCd] || post.categoryCd}
</span>
{post.pinnedYn === 'Y' && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded text-caption font-semibold bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-color-accent">
📌
</span>
)}
</div>
<h1 className="text-title-1 font-bold text-fg mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-label-1 text-fg-disabled">
<span>
: <span className="text-fg-sub">{post.authorName}</span>
</span>
<span>|</span>
<span>: {new Date(post.regDtm).toLocaleDateString('ko-KR')}</span>
<span>|</span>
<span>: {post.viewCnt}</span>
{post.mdfcnDtm && (
<>
<span>|</span>
<span>: {new Date(post.mdfcnDtm).toLocaleDateString('ko-KR')}</span>
</>
)}
</div>
</div>
{/* 본문 */}
<div className="py-8">
<div className="prose prose-invert max-w-none">
<div className="text-fg text-subtitle leading-relaxed whitespace-pre-wrap">
{post.content || '(내용 없음)'}
</div>
</div>
</div>
{/* 댓글 섹션 (향후 구현 예정) */}
<div className="py-6 border-t border-stroke">
<div className="text-center py-8">
<p className="text-fg-disabled text-label-1"> .</p>
</div>
</div>
</div>
</div>
</div>
);
}