import { useState, useEffect } from 'react' import { sanitizeInput } from '@common/utils/sanitize' import { fetchBoardPost, createBoardPost, updateBoardPost, } from '../services/boardApi' interface BoardWriteFormProps { postSn?: number | null defaultCategoryCd: string onSaveComplete: () => void onCancel: () => void } // DB 카테고리 코드 ↔ 표시명 const CATEGORY_OPTIONS: Array<{ code: string; label: string }> = [ { code: 'NOTICE', label: '공지사항' }, { code: 'DATA', label: '자료실' }, { code: 'QNA', label: 'Q&A' }, ] // 허용된 파일 확장자 const ALLOWED_FILE_EXTENSIONS = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.hwp', '.png', '.jpg', '.jpeg'] const MAX_FILE_SIZE_MB = 10 export function BoardWriteForm({ postSn, defaultCategoryCd, onSaveComplete, onCancel }: BoardWriteFormProps) { const isEditMode = postSn != null const [categoryCd, setCategoryCd] = useState(defaultCategoryCd) const [title, setTitle] = useState('') const [content, setContent] = useState('') const [isLoading, setIsLoading] = useState(false) const [isFetching, setIsFetching] = useState(false) // 수정 모드: 기존 게시글 데이터 로드 useEffect(() => { if (!isEditMode) return let cancelled = false const load = async () => { setIsFetching(true) try { const post = await fetchBoardPost(postSn!) if (cancelled) return setCategoryCd(post.categoryCd) setTitle(post.title) setContent(post.content || '') } catch { if (!cancelled) { alert('게시글을 불러오는데 실패했습니다.') onCancel() } } finally { if (!cancelled) setIsFetching(false) } } load() return () => { cancelled = true } }, [postSn, isEditMode, onCancel]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // 입력값 살균 (XSS 방지) const safeTitle = sanitizeInput(title, 200) const safeContent = sanitizeInput(content, 10000) if (!safeTitle) { alert('제목을 입력해주세요.') return } if (!safeContent) { alert('내용을 입력해주세요.') return } setIsLoading(true) try { if (isEditMode) { await updateBoardPost(postSn!, { title: safeTitle, content: safeContent }) alert('게시글이 수정되었습니다.') } else { await createBoardPost({ categoryCd, title: safeTitle, content: safeContent }) alert('게시글이 등록되었습니다.') } onSaveComplete() } catch (err) { alert((err as { message?: string })?.message || '저장에 실패했습니다.') } finally { setIsLoading(false) } } // 파일 업로드 보안 검증 const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files if (!files) return for (const file of Array.from(files)) { if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { alert(`파일 크기 초과: ${file.name} (최대 ${MAX_FILE_SIZE_MB}MB)`) e.target.value = '' return } const ext = '.' + (file.name.split('.').pop()?.toLowerCase() || '') if (!ALLOWED_FILE_EXTENSIONS.includes(ext)) { alert(`허용되지 않는 파일 형식: ${file.name}`) e.target.value = '' return } } } if (isFetching) { return (

게시글을 불러오는 중...

) } return (
{/* 헤더 */}

{isEditMode ? '게시글 수정' : '게시글 작성'}

{/* 폼 */}
{/* 분류 선택 */}
{/* 제목 */}
setTitle(e.target.value)} maxLength={200} placeholder="제목을 입력하세요" className="w-full px-4 py-2.5 text-sm bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 focus:border-primary-cyan focus:outline-none" />
{/* 내용 */}