summaryrefslogtreecommitdiff
path: root/lib/bidding/selection/selection-result-form.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/selection/selection-result-form.tsx')
-rw-r--r--lib/bidding/selection/selection-result-form.tsx213
1 files changed, 169 insertions, 44 deletions
diff --git a/lib/bidding/selection/selection-result-form.tsx b/lib/bidding/selection/selection-result-form.tsx
index 54687cc9..af6b8d43 100644
--- a/lib/bidding/selection/selection-result-form.tsx
+++ b/lib/bidding/selection/selection-result-form.tsx
@@ -9,8 +9,8 @@ import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { useToast } from '@/hooks/use-toast'
-import { saveSelectionResult } from './actions'
-import { Loader2, Save, FileText } from 'lucide-react'
+import { saveSelectionResult, getSelectionResult } from './actions'
+import { Loader2, Save, FileText, Download, X } from 'lucide-react'
import { Dropzone, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription, DropzoneInput } from '@/components/ui/dropzone'
const selectionResultSchema = z.object({
@@ -22,12 +22,25 @@ type SelectionResultFormData = z.infer<typeof selectionResultSchema>
interface SelectionResultFormProps {
biddingId: number
onSuccess: () => void
+ readOnly?: boolean
}
-export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFormProps) {
+interface AttachmentInfo {
+ id: number
+ fileName: string
+ originalFileName: string
+ fileSize: number
+ mimeType: string
+ filePath: string
+ uploadedAt: Date | null
+}
+
+export function SelectionResultForm({ biddingId, onSuccess, readOnly = false }: SelectionResultFormProps) {
const { toast } = useToast()
const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const [isLoading, setIsLoading] = React.useState(true)
const [attachmentFiles, setAttachmentFiles] = React.useState<File[]>([])
+ const [existingAttachments, setExistingAttachments] = React.useState<AttachmentInfo[]>([])
const form = useForm<SelectionResultFormData>({
resolver: zodResolver(selectionResultSchema),
@@ -36,10 +49,53 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
},
})
+ // 기존 선정결과 로드
+ React.useEffect(() => {
+ const loadSelectionResult = async () => {
+ setIsLoading(true)
+ try {
+ const result = await getSelectionResult(biddingId)
+ if (result.success && result.data) {
+ form.reset({
+ summary: result.data.summary || '',
+ })
+ if (result.data.attachments) {
+ setExistingAttachments(result.data.attachments)
+ }
+ }
+ } catch (error) {
+ console.error('Failed to load selection result:', error)
+ toast({
+ title: '로드 실패',
+ description: '선정결과를 불러오는데 실패했습니다.',
+ variant: 'destructive',
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadSelectionResult()
+ }, [biddingId, form, toast])
+
const removeAttachmentFile = (index: number) => {
setAttachmentFiles(prev => prev.filter((_, i) => i !== index))
}
+ const removeExistingAttachment = (id: number) => {
+ setExistingAttachments(prev => prev.filter(att => att.id !== id))
+ }
+
+ const downloadAttachment = (filePath: string, fileName: string) => {
+ // 파일 다운로드 (filePath가 절대 경로인 경우)
+ if (filePath.startsWith('http') || filePath.startsWith('/')) {
+ window.open(filePath, '_blank')
+ } else {
+ // 상대 경로인 경우
+ window.open(`/api/files/${filePath}`, '_blank')
+ }
+ }
+
const onSubmit = async (data: SelectionResultFormData) => {
setIsSubmitting(true)
try {
@@ -74,6 +130,22 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
}
}
+ if (isLoading) {
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle>선정결과</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="flex items-center justify-center py-8">
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
+ <span className="ml-2 text-sm text-muted-foreground">로딩 중...</span>
+ </div>
+ </CardContent>
+ </Card>
+ )
+ }
+
return (
<Card>
<CardHeader>
@@ -94,6 +166,7 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
placeholder="선정결과에 대한 요약을 입력해주세요..."
className="min-h-[120px]"
{...field}
+ disabled={readOnly}
/>
</FormControl>
<FormMessage />
@@ -104,35 +177,83 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
{/* 첨부파일 */}
<div className="space-y-4">
<FormLabel>첨부파일</FormLabel>
- <Dropzone
- maxSize={10 * 1024 * 1024} // 10MB
- onDropAccepted={(files) => {
- const newFiles = Array.from(files)
- setAttachmentFiles(prev => [...prev, ...newFiles])
- }}
- onDropRejected={() => {
- toast({
- title: "파일 업로드 거부",
- description: "파일 크기 및 형식을 확인해주세요.",
- variant: "destructive",
- })
- }}
- >
- <DropzoneZone>
- <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
- <DropzoneTitle className="text-lg font-medium">
- 파일을 드래그하거나 클릭하여 업로드
- </DropzoneTitle>
- <DropzoneDescription className="text-sm text-muted-foreground">
- PDF, Word, Excel, 이미지 파일 (최대 10MB)
- </DropzoneDescription>
- </DropzoneZone>
- <DropzoneInput />
- </Dropzone>
+
+ {/* 기존 첨부파일 */}
+ {existingAttachments.length > 0 && (
+ <div className="space-y-2">
+ <h4 className="text-sm font-medium">기존 첨부파일</h4>
+ <div className="space-y-2">
+ {existingAttachments.map((attachment) => (
+ <div
+ key={attachment.id}
+ className="flex items-center justify-between p-3 bg-muted rounded-lg"
+ >
+ <div className="flex items-center gap-3">
+ <FileText className="h-4 w-4 text-muted-foreground" />
+ <div>
+ <p className="text-sm font-medium">{attachment.originalFileName || attachment.fileName}</p>
+ <p className="text-xs text-muted-foreground">
+ {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB
+ </p>
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => downloadAttachment(attachment.filePath, attachment.originalFileName || attachment.fileName)}
+ >
+ <Download className="h-4 w-4" />
+ </Button>
+ {!readOnly && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeExistingAttachment(attachment.id)}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {!readOnly && (
+ <Dropzone
+ maxSize={10 * 1024 * 1024} // 10MB
+ onDropAccepted={(files) => {
+ const newFiles = Array.from(files)
+ setAttachmentFiles(prev => [...prev, ...newFiles])
+ }}
+ onDropRejected={() => {
+ toast({
+ title: "파일 업로드 거부",
+ description: "파일 크기 및 형식을 확인해주세요.",
+ variant: "destructive",
+ })
+ }}
+ >
+ <DropzoneZone>
+ <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+ <DropzoneTitle className="text-lg font-medium">
+ 파일을 드래그하거나 클릭하여 업로드
+ </DropzoneTitle>
+ <DropzoneDescription className="text-sm text-muted-foreground">
+ PDF, Word, Excel, 이미지 파일 (최대 10MB)
+ </DropzoneDescription>
+ </DropzoneZone>
+ <DropzoneInput />
+ </Dropzone>
+ )}
{attachmentFiles.length > 0 && (
<div className="space-y-2">
- <h4 className="text-sm font-medium">업로드된 파일</h4>
+ <h4 className="text-sm font-medium">새로 추가할 파일</h4>
<div className="space-y-2">
{attachmentFiles.map((file, index) => (
<div
@@ -148,14 +269,16 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
</p>
</div>
</div>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeAttachmentFile(index)}
- >
- 제거
- </Button>
+ {!readOnly && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="sm"
+ onClick={() => removeAttachmentFile(index)}
+ >
+ 제거
+ </Button>
+ )}
</div>
))}
</div>
@@ -164,13 +287,15 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
</div>
{/* 저장 버튼 */}
- <div className="flex justify-end">
- <Button type="submit" disabled={isSubmitting}>
- {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- <Save className="mr-2 h-4 w-4" />
- 저장
- </Button>
- </div>
+ {!readOnly && (
+ <div className="flex justify-end">
+ <Button type="submit" disabled={isSubmitting}>
+ {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ <Save className="mr-2 h-4 w-4" />
+ 저장
+ </Button>
+ </div>
+ )}
</form>
</Form>
</CardContent>