import { useState, useEffect, useCallback } from 'react' import { useSubMenu } from '@common/hooks/useSubMenu' import { useAuthStore } from '@common/store/authStore' import { BoardWriteForm } from './BoardWriteForm' import { BoardDetailView } from './BoardDetailView' import { fetchBoardPosts, deleteBoardPost, fetchManuals, createManual, updateManual, deleteManual, incrementManualDownload, type BoardPostItem, type ManualItem, } from '../services/boardApi' type ViewMode = 'list' | 'detail' | 'write' // 서브탭 → DB 카테고리 코드 매핑 const SUB_TAB_TO_CATEGORY: Record = { all: undefined, notice: 'NOTICE', data: 'DATA', qna: 'QNA', } // 카테고리 코드 → 표시명 const CATEGORY_LABELS: Record = { NOTICE: '공지사항', DATA: '자료실', QNA: 'Q&A', MANUAL: '해경매뉴얼', } // 카테고리별 배지 색상 const CATEGORY_COLORS: Record = { NOTICE: 'bg-red-500/20 text-red-400', DATA: 'bg-green-500/20 text-green-400', QNA: 'bg-purple-500/20 text-purple-400', MANUAL: 'bg-blue-500/20 text-blue-400', } const PAGE_SIZE = 20 export function BoardView() { const { activeSubTab } = useSubMenu('board') const hasPermission = useAuthStore((s) => s.hasPermission) // 목록 상태 const [posts, setPosts] = useState([]) const [totalCount, setTotalCount] = useState(0) const [page, setPage] = useState(1) const [searchTerm, setSearchTerm] = useState('') const [isLoading, setIsLoading] = useState(false) // 뷰 상태 const [viewMode, setViewMode] = useState('list') const [selectedPostSn, setSelectedPostSn] = useState(null) const [editingPostSn, setEditingPostSn] = useState(null) const categoryCd = SUB_TAB_TO_CATEGORY[activeSubTab] // 게시글 목록 조회 const loadPosts = useCallback(async () => { setIsLoading(true) try { const result = await fetchBoardPosts({ categoryCd, search: searchTerm || undefined, page, size: PAGE_SIZE, }) setPosts(result.items) setTotalCount(result.totalCount) } catch (err) { console.error('[board] 목록 조회 실패:', err) } finally { setIsLoading(false) } }, [categoryCd, searchTerm, page]) useEffect(() => { if (activeSubTab !== 'manual') { loadPosts() } }, [loadPosts, activeSubTab]) // 필터 변경 시 페이지 초기화 useEffect(() => { setPage(1) }, [activeSubTab]) // 상세 보기 const handlePostClick = (sn: number) => { setSelectedPostSn(sn) setViewMode('detail') } // 글쓰기 const handleWriteClick = () => { setEditingPostSn(null) setViewMode('write') } // 상세에서 수정 const handleEditFromDetail = (sn: number) => { setEditingPostSn(sn) setViewMode('write') } // 삭제 (목록 또는 상세에서) const handleDelete = async (sn: number) => { if (!window.confirm('정말로 이 게시글을 삭제하시겠습니까?')) return try { await deleteBoardPost(sn) alert('게시글이 삭제되었습니다.') setViewMode('list') setSelectedPostSn(null) loadPosts() } catch (err) { alert((err as { message?: string })?.message || '삭제에 실패했습니다.') } } // 목록으로 돌아가기 const handleBackToList = () => { setViewMode('list') setSelectedPostSn(null) setEditingPostSn(null) loadPosts() } // 저장 완료 후 목록으로 const handleSaveComplete = () => { setViewMode('list') setEditingPostSn(null) loadPosts() } // 검색 Enter 키 const handleSearchKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { setPage(1) loadPosts() } } // 현재 서브탭 기준 글쓰기 권한 리소스 const getWriteResource = () => { if (activeSubTab === 'all') return 'board' return `board:${activeSubTab}` } const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) /* ══════════════════════════════════════════════ 해경매뉴얼 탭 (mock, 향후 API 전환 예정) ══════════════════════════════════════════════ */ const [manualCategory, setManualCategory] = useState('전체') const [manualSearch, setManualSearch] = useState('') const [manualList, setManualList] = useState([]) const [manualLoading, setManualLoading] = useState(false) const [showUploadModal, setShowUploadModal] = useState(false) const [editingManualId, setEditingManualId] = useState(null) const [uploadForm, setUploadForm] = useState({ category: '방제매뉴얼', title: '', version: '', fileName: '', fileSize: '' }) const manualCategories = ['전체', '방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'] const loadManuals = useCallback(async () => { setManualLoading(true) try { const items = await fetchManuals({ category: manualCategory !== '전체' ? manualCategory : undefined, search: manualSearch || undefined, }) setManualList(items) } catch (err) { console.error('[board] 매뉴얼 목록 조회 실패:', err) } finally { setManualLoading(false) } }, [manualCategory, manualSearch]) useEffect(() => { if (activeSubTab === 'manual') { loadManuals() } }, [loadManuals, activeSubTab]) const filteredManuals = manualList const catColor = (cat: string) => { switch (cat) { case '방제매뉴얼': return { bg: 'rgba(6,182,212,.15)', text: '#22d3ee' } case '대응매뉴얼': return { bg: 'rgba(249,115,22,.15)', text: '#f97316' } case '교육자료': return { bg: 'rgba(34,197,94,.15)', text: '#22c55e' } case '법령·규정': return { bg: 'rgba(168,85,247,.15)', text: '#a855f7' } default: return { bg: 'rgba(100,100,100,.15)', text: '#999' } } } if (activeSubTab === 'manual') { return (
{/* 헤더 */}
📘 해경매뉴얼 총 {filteredManuals.length}건
{manualCategories.map(cat => ( ))}
setManualSearch(e.target.value)} className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg-elevated)', border: '1px solid var(--stroke-default)', outline: 'none' }} />
{/* 그리드 */}
{manualLoading ? (

로딩 중...

) : (
{filteredManuals.map(file => { const cc = catColor(file.catgNm) return (
{ (e.currentTarget as HTMLElement).style.borderColor = 'rgba(6,182,212,.4)' }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--stroke-default)' }} >
{file.catgNm} {file.version}
{file.title}
📄 {file.fileTp || 'PDF'}
{file.fileSz}
{file.authorNm} {new Date(file.regDtm).toLocaleDateString('ko-KR')}
⬇ {file.dwnldCnt}
) })}
)} {!manualLoading && filteredManuals.length === 0 && (
📘

검색 결과가 없습니다.

)}
{/* 업로드 모달 */} {showUploadModal && (
{ setShowUploadModal(false); setEditingManualId(null) }}>
e.stopPropagation()}>
{editingManualId ? '✏️' : '📤'} {editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}
{ setShowUploadModal(false); setEditingManualId(null) }} className="cursor-pointer text-fg-disabled text-base leading-none">✕
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map(cat => { const cc = catColor(cat) const isActive = uploadForm.category === cat return ( ) })}
setUploadForm(prev => ({ ...prev, title: e.target.value }))} className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none" style={{ boxSizing: 'border-box' }} />
setUploadForm(prev => ({ ...prev, version: e.target.value }))} className="w-full px-3 py-2.5 rounded-md text-xs bg-bg-elevated border border-stroke outline-none" style={{ boxSizing: 'border-box' }} />
{ const input = document.createElement('input') input.type = 'file' input.accept = '.pdf,.doc,.docx,.hwp,.xlsx' input.onchange = (ev) => { const f = (ev.target as HTMLInputElement).files?.[0] if (f) { const sizeMB = (f.size / (1024 * 1024)).toFixed(1) setUploadForm(prev => ({ ...prev, fileName: f.name, fileSize: `${sizeMB} MB` })) } } input.click() }}> {uploadForm.fileName ? (
📄
{uploadForm.fileName}
{uploadForm.fileSize}
{ e.stopPropagation(); setUploadForm(prev => ({ ...prev, fileName: '', fileSize: '' })) }} className="text-xs text-fg-disabled cursor-pointer ml-2">✕
) : ( <>
📁
클릭하여 파일을 선택하세요
PDF, DOC, HWP, XLSX (최대 100MB)
)}
)}
) } /* ══════════════════════════════════════════════ 상세 보기 ══════════════════════════════════════════════ */ if (viewMode === 'detail' && selectedPostSn) { return (
handleEditFromDetail(selectedPostSn)} onDelete={() => handleDelete(selectedPostSn)} />
) } /* ══════════════════════════════════════════════ 글쓰기 / 수정 ══════════════════════════════════════════════ */ if (viewMode === 'write') { return (
) } /* ══════════════════════════════════════════════ 목록 보기 ══════════════════════════════════════════════ */ return (
{/* 헤더 */}
{totalCount}
setSearchTerm(e.target.value)} onKeyDown={handleSearchKeyDown} className="px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none w-64" /> {hasPermission(getWriteResource(), 'CREATE') && ( )}
{/* 테이블 */}
{isLoading ? (

로딩 중...

) : ( <> {posts.map((post) => ( ))}
번호 분류 제목 작성자 작성일 조회
{post.sn} {CATEGORY_LABELS[post.categoryCd] || post.categoryCd} handlePostClick(post.sn)} > {post.pinnedYn === 'Y' && '📌 '}{post.title} {post.authorName} {new Date(post.regDtm).toLocaleDateString('ko-KR')} {post.viewCnt}
{posts.length === 0 && (

게시글이 없습니다.

)} )}
{/* 페이지네이션 */} {totalPages > 1 && (
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( ))}
)}
) }