'use client'; /** * 게시글 등록/수정 폼 컴포넌트 * IntegratedDetailTemplate 마이그레이션 (2025-01-20) * * 디자인 스펙 기준: * - 페이지 타이틀: 게시글 상세 * - 페이지 설명: 게시글을 등록하고 관리합니다. * - 필드: 게시판, 상단 노출, 제목, 내용(에디터), 첨부파일, 작성자, 댓글, 등록일시 */ import { useState, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { FileDropzone } from '@/components/ui/file-dropzone'; import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { boardCreateConfig, boardEditConfig } from './boardFormConfig'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { RichTextEditor } from '../RichTextEditor'; import type { Post, Attachment } from '../types'; import { createPost, updatePost } from '../actions'; import { getBoards } from '../BoardManagement/actions'; import type { Board } from '../BoardManagement/types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; interface BoardFormProps { mode: 'create' | 'edit'; initialData?: Post; } // 현재 로그인 사용자 정보 (실제로는 auth context에서 가져옴) const CURRENT_USER = { id: 'user1', name: '홍길동', department: '개발팀', position: '과장', }; // 상단 고정 최대 개수 const MAX_PINNED_COUNT = 5; export function BoardForm({ mode, initialData }: BoardFormProps) { const router = useRouter(); // ===== 폼 상태 ===== const [boardCode, setBoardCode] = useState(initialData?.boardCode || ''); const [isPinned, setIsPinned] = useState(initialData?.isPinned ? 'true' : 'false'); const [title, setTitle] = useState(initialData?.title || ''); const [content, setContent] = useState(initialData?.content || ''); const [allowComments, setAllowComments] = useState(initialData?.allowComments ? 'true' : 'false'); const [attachments, setAttachments] = useState([]); const [existingAttachments, setExistingAttachments] = useState( (initialData?.attachments || []).map(a => ({ id: a.id, name: a.fileName, url: a.fileUrl, size: a.fileSize, })) ); // 상단 노출 초과 Alert const [showPinnedAlert, setShowPinnedAlert] = useState(false); // 유효성 에러 const [errors, setErrors] = useState>({}); // 게시판 목록 상태 const [boards, setBoards] = useState([]); const [isBoardsLoading, setIsBoardsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); // ===== 게시판 목록 조회 ===== useEffect(() => { async function fetchBoards() { setIsBoardsLoading(true); try { const result = await getBoards(); if (result.success && result.data) { setBoards(result.data); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('게시판 목록 조회 오류:', error); toast.error('게시판 목록을 불러오지 못했습니다.'); } finally { setIsBoardsLoading(false); } } fetchBoards(); }, []); // ===== 상단 노출 변경 핸들러 ===== const handlePinnedChange = useCallback((value: string) => { if (value === 'true') { // 상단 노출 5개 제한 체크 (Mock: 현재 3개 고정중이라고 가정) const currentPinnedCount = 3; if (currentPinnedCount >= MAX_PINNED_COUNT) { setShowPinnedAlert(true); return; } } setIsPinned(value); }, []); // ===== 파일 업로드 핸들러 ===== const handleFilesSelect = useCallback((files: File[]) => { const newFiles = files.map(file => ({ file })); setAttachments((prev) => [...prev, ...newFiles]); }, []); const handleRemoveFile = useCallback((index: number) => { setAttachments((prev) => prev.filter((_, i) => i !== index)); }, []); const handleRemoveExistingFile = useCallback((id: string | number) => { setExistingAttachments((prev) => prev.filter((a) => a.id !== id)); }, []); // ===== 유효성 검사 ===== const validate = useCallback(() => { const newErrors: Record = {}; if (!boardCode) { newErrors.boardCode = '게시판을 선택해주세요.'; } if (!title.trim()) { newErrors.title = '제목을 입력해주세요.'; } if (!content.trim() || content === '

') { newErrors.content = '내용을 입력해주세요.'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }, [boardCode, title, content]); // ===== 저장 핸들러 ===== const handleSubmit = useCallback(async () => { if (!validate()) return; setIsSubmitting(true); try { const postData = { title, content, is_notice: isPinned === 'true', is_secret: false, }; let result; if (mode === 'create') { result = await createPost(boardCode, postData); } else if (initialData) { result = await updatePost(boardCode, initialData.id, postData); } if (result?.success) { toast.success(mode === 'create' ? '게시글이 등록되었습니다.' : '게시글이 수정되었습니다.'); router.push('/ko/board'); } else { toast.error(result?.error || '게시글 저장에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('게시글 저장 오류:', error); toast.error('게시글 저장에 실패했습니다.'); } finally { setIsSubmitting(false); } }, [boardCode, title, content, isPinned, mode, initialData, router, validate]); // ===== 취소 핸들러 ===== const handleCancel = useCallback(() => { router.back(); }, [router]); // ===== 폼 콘텐츠 렌더링 ===== const renderFormContent = useCallback(() => ( <> {/* 폼 카드 */} 게시글 정보 * 게시글의 기본 정보를 입력해주세요. {/* 게시판 선택 */}
{errors.boardCode && (

{errors.boardCode}

)}
{/* 상단 노출 */}
{/* 제목 */}
setTitle(e.target.value)} placeholder="제목을 입력해주세요" className={errors.title ? 'border-red-500' : ''} /> {errors.title && (

{errors.title}

)}
{/* 내용 (에디터) */}
{errors.content && (

{errors.content}

)}
{/* 첨부파일 */}
{/* 작성자 (읽기 전용) */}
{/* 등록일시 (수정 모드에서만) */} {mode === 'edit' && initialData && (
)}
{/* 댓글 */}
{/* 등록일시 (등록 모드) */} {mode === 'create' && (
)}
{/* 상단 노출 초과 Alert */} 상단 노출 제한 상단 노출은 5개까지 설정 가능합니다. setShowPinnedAlert(false)}> 확인 ), [ boardCode, isPinned, title, content, allowComments, errors, boards, isBoardsLoading, mode, initialData, attachments, existingAttachments, showPinnedAlert, handlePinnedChange, handleFilesSelect, handleRemoveFile, handleRemoveExistingFile, ]); // Config 선택 (create/edit) const config = mode === 'create' ? boardCreateConfig : boardEditConfig; return ( ); } export default BoardForm;