import { Router } from 'express' import { requireAuth, requirePermission } from '../auth/authMiddleware.js' import { AuthError } from '../auth/authService.js' import { listPosts, getPost, createPost, updatePost, deletePost, adminDeletePost, listManuals, createManual, updateManual, deleteManual, incrementManualDownload, } from './boardService.js' const router = Router() // 카테고리 → 리소스 매핑 const CATEGORY_RESOURCE: Record = { NOTICE: 'board:notice', DATA: 'board:data', QNA: 'board:qna', MANUAL: 'board:manual', } // ============================================================ // 매뉴얼 라우트 (/:sn 보다 먼저 등록해야 함) // ============================================================ // GET /api/board/manual — 매뉴얼 목록 router.get('/manual', requireAuth, requirePermission('board:manual', 'READ'), async (req, res) => { try { const { category, search } = req.query const items = await listManuals({ category: category as string | undefined, search: search as string | undefined, }) res.json(items) } catch (err) { console.error('[board] 매뉴얼 목록 오류:', err) res.status(500).json({ error: '매뉴얼 목록 조회 중 오류가 발생했습니다.' }) } }) // POST /api/board/manual — 매뉴얼 등록 router.post('/manual', requireAuth, requirePermission('board:manual', 'CREATE'), async (req, res) => { try { const { catgNm, title, version, fileTp, fileSz, filePath, authorNm } = req.body if (!catgNm || !title) { res.status(400).json({ error: '카테고리와 제목은 필수입니다.' }) return } const result = await createManual({ catgNm, title, version, fileTp, fileSz, filePath, authorNm }) res.status(201).json(result) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }); return } console.error('[board] 매뉴얼 등록 오류:', err) res.status(500).json({ error: '매뉴얼 등록 중 오류가 발생했습니다.' }) } }) // PUT /api/board/manual/:sn — 매뉴얼 수정 router.put('/manual/:sn', requireAuth, requirePermission('board:manual', 'UPDATE'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 매뉴얼 번호입니다.' }); return } const { catgNm, title, version, fileTp, fileSz, filePath } = req.body await updateManual(sn, { catgNm, title, version, fileTp, fileSz, filePath }) res.json({ success: true }) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }); return } console.error('[board] 매뉴얼 수정 오류:', err) res.status(500).json({ error: '매뉴얼 수정 중 오류가 발생했습니다.' }) } }) // DELETE /api/board/manual/:sn — 매뉴얼 삭제 router.delete('/manual/:sn', requireAuth, requirePermission('board:manual', 'DELETE'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 매뉴얼 번호입니다.' }); return } await deleteManual(sn) res.json({ success: true }) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }); return } console.error('[board] 매뉴얼 삭제 오류:', err) res.status(500).json({ error: '매뉴얼 삭제 중 오류가 발생했습니다.' }) } }) // POST /api/board/manual/:sn/download — 매뉴얼 다운로드 카운트 증가 router.post('/manual/:sn/download', requireAuth, requirePermission('board:manual', 'READ'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 매뉴얼 번호입니다.' }); return } await incrementManualDownload(sn) res.json({ success: true }) } catch (err) { console.error('[board] 다운로드 카운트 오류:', err) res.status(500).json({ error: '다운로드 처리 중 오류가 발생했습니다.' }) } }) // ============================================================ // 게시글 라우트 // ============================================================ // GET /api/board — 게시글 목록 router.get('/', requireAuth, requirePermission('board', 'READ'), async (req, res) => { try { const { categoryCd, search, page, size } = req.query const result = await listPosts({ categoryCd: categoryCd as string | undefined, search: search as string | undefined, page: page ? parseInt(page as string, 10) : undefined, size: size ? parseInt(size as string, 10) : undefined, }) res.json(result) } catch (err) { console.error('[board] 목록 조회 오류:', err) res.status(500).json({ error: '게시글 목록 조회 중 오류가 발생했습니다.' }) } }) // GET /api/board/:sn — 게시글 상세 router.get('/:sn', requireAuth, requirePermission('board', 'READ'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 게시글 번호입니다.' }) return } const post = await getPost(sn) res.json(post) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }) return } console.error('[board] 상세 조회 오류:', err) res.status(500).json({ error: '게시글 조회 중 오류가 발생했습니다.' }) } }) // POST /api/board — 게시글 작성 (카테고리별 CREATE 권한) router.post('/', requireAuth, async (req, res, next) => { const resource = CATEGORY_RESOURCE[req.body.categoryCd] || 'board' requirePermission(resource, 'CREATE')(req, res, next) }, async (req, res) => { try { const { categoryCd, title, content, pinnedYn } = req.body if (!categoryCd || !title) { res.status(400).json({ error: '카테고리와 제목은 필수입니다.' }) return } const result = await createPost({ categoryCd, title, content, authorId: req.user!.sub, pinnedYn, }) res.status(201).json(result) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }) return } console.error('[board] 작성 오류:', err) res.status(500).json({ error: '게시글 작성 중 오류가 발생했습니다.' }) } }) // PUT /api/board/:sn — 게시글 수정 (소유자 검증은 서비스에서) router.put('/:sn', requireAuth, requirePermission('board', 'UPDATE'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 게시글 번호입니다.' }) return } const { title, content, pinnedYn } = req.body await updatePost(sn, { title, content, pinnedYn }, req.user!.sub) res.json({ success: true }) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }) return } console.error('[board] 수정 오류:', err) res.status(500).json({ error: '게시글 수정 중 오류가 발생했습니다.' }) } }) // DELETE /api/board/:sn — 게시글 삭제 (논리 삭제, 소유자 검증) router.delete('/:sn', requireAuth, requirePermission('board', 'DELETE'), async (req, res) => { try { const sn = parseInt(req.params.sn as string, 10) if (isNaN(sn)) { res.status(400).json({ error: '유효하지 않은 게시글 번호입니다.' }) return } await deletePost(sn, req.user!.sub) res.json({ success: true }) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }) return } console.error('[board] 삭제 오류:', err) res.status(500).json({ error: '게시글 삭제 중 오류가 발생했습니다.' }) } }) // POST /api/board/admin-delete — 관리자 전용 게시글 삭제 (소유자 검증 없음) router.post('/admin-delete', requireAuth, requirePermission('admin', 'READ'), async (req, res) => { try { const { sn } = req.body const postSn = typeof sn === 'number' ? sn : parseInt(sn, 10) if (isNaN(postSn)) { res.status(400).json({ error: '유효하지 않은 게시글 번호입니다.' }) return } await adminDeletePost(postSn) res.json({ success: true }) } catch (err) { if (err instanceof AuthError) { res.status(err.status).json({ error: err.message }); return } console.error('[board] 관리자 삭제 오류:', err) res.status(500).json({ error: '게시글 삭제 중 오류가 발생했습니다.' }) } }) export default router