refactor: 모달 Content 컴포넌트 분리 및 파일 입력 UI 공통화
- 모달 컴포넌트에서 Content 분리하여 재사용성 향상 - EstimateDocumentContent, DirectConstructionContent 등 - WorkLogContent, QuotePreviewContent, ReceivingReceiptContent - 파일 입력 공통 UI 컴포넌트 추가 - file-dropzone, file-input, file-list, image-upload - 폼 컴포넌트 코드 정리 및 중복 제거 (-4,056줄) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,13 @@
|
||||
* - 필드: 게시판, 상단 노출, 제목, 내용(에디터), 첨부파일, 작성자, 댓글, 등록일시
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { Upload, X, File, Loader2 } from 'lucide-react';
|
||||
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';
|
||||
@@ -70,7 +72,6 @@ const MAX_PINNED_COUNT = 5;
|
||||
|
||||
export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// ===== 폼 상태 =====
|
||||
const [boardCode, setBoardCode] = useState(initialData?.boardCode || '');
|
||||
@@ -78,9 +79,14 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
const [title, setTitle] = useState(initialData?.title || '');
|
||||
const [content, setContent] = useState(initialData?.content || '');
|
||||
const [allowComments, setAllowComments] = useState(initialData?.allowComments ? 'true' : 'false');
|
||||
const [attachments, setAttachments] = useState<File[]>([]);
|
||||
const [existingAttachments, setExistingAttachments] = useState<Attachment[]>(
|
||||
initialData?.attachments || []
|
||||
const [attachments, setAttachments] = useState<NewFile[]>([]);
|
||||
const [existingAttachments, setExistingAttachments] = useState<ExistingFile[]>(
|
||||
(initialData?.attachments || []).map(a => ({
|
||||
id: a.id,
|
||||
name: a.fileName,
|
||||
url: a.fileUrl,
|
||||
size: a.fileSize,
|
||||
}))
|
||||
);
|
||||
|
||||
// 상단 노출 초과 Alert
|
||||
@@ -128,22 +134,16 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
}, []);
|
||||
|
||||
// ===== 파일 업로드 핸들러 =====
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
setAttachments((prev) => [...prev, ...Array.from(files)]);
|
||||
}
|
||||
// 파일 input 초기화
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.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) => {
|
||||
const handleRemoveExistingFile = useCallback((id: string | number) => {
|
||||
setExistingAttachments((prev) => prev.filter((a) => a.id !== id));
|
||||
}, []);
|
||||
|
||||
@@ -206,13 +206,6 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
// 파일 크기 포맷
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
// ===== 폼 콘텐츠 렌더링 =====
|
||||
const renderFormContent = useCallback(() => (
|
||||
<>
|
||||
@@ -314,80 +307,21 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
{/* 첨부파일 */}
|
||||
<div className="space-y-2">
|
||||
<Label>첨부파일</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
찾기
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기존 파일 목록 */}
|
||||
{existingAttachments.length > 0 && (
|
||||
<div className="space-y-2 mt-3">
|
||||
{existingAttachments.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-2 bg-gray-50 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<File className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm">{file.fileName}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({formatFileSize(file.fileSize)})
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveExistingFile(file.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 새로 추가된 파일 목록 */}
|
||||
{attachments.length > 0 && (
|
||||
<div className="space-y-2 mt-3">
|
||||
{attachments.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-2 bg-blue-50 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<File className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm">{file.name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({formatFileSize(file.size)})
|
||||
</span>
|
||||
<span className="text-xs text-blue-500">(새 파일)</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveFile(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<FileDropzone
|
||||
onFilesSelect={handleFilesSelect}
|
||||
multiple
|
||||
maxSize={10}
|
||||
compact
|
||||
title="클릭하거나 파일을 드래그하세요"
|
||||
description="최대 10MB"
|
||||
/>
|
||||
<FileList
|
||||
files={attachments}
|
||||
existingFiles={existingAttachments}
|
||||
onRemove={handleRemoveFile}
|
||||
onRemoveExisting={handleRemoveExistingFile}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 작성자 (읽기 전용) */}
|
||||
@@ -471,7 +405,7 @@ export function BoardForm({ mode, initialData }: BoardFormProps) {
|
||||
), [
|
||||
boardCode, isPinned, title, content, allowComments, errors, boards,
|
||||
isBoardsLoading, mode, initialData, attachments, existingAttachments,
|
||||
showPinnedAlert, formatFileSize, handlePinnedChange, handleFileSelect,
|
||||
showPinnedAlert, handlePinnedChange, handleFilesSelect,
|
||||
handleRemoveFile, handleRemoveExistingFile,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user