File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import { useState } from 'react';
import { FoxFormLabel } from '@fox/core/components/fox-form-label';
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
import styles from './board-attachment-field.module.scss';
import {
FILE_MODULE,
MAX_ATTACHMENT_BYTES,
allowedExtensions,
fileModuleAccept,
isAllowedFileExtension,
} from '@/lib/domain/file-module';
interface BoardAttachmentFieldProps {
label?: string;
/** 이미 저장돼 있는 첨부파일 식별자(수정 화면). 없으면 신규 등록이다. */
attachmentId?: string | null;
}
/** 다운로드 라우트 — 백엔드 파일 API를 중계한다(`boards/attachments/route.ts`). */
const ATTACHMENT_DOWNLOAD_PATH = '/boards/attachments';
/**
* 첨부파일 입력 — 파일을 고르면 **폼과 함께 그대로 제출**되고, Server Action이 업로드한 뒤
* 게시물에 연결한다(브라우저에서 미리 업로드하지 않는다 — `_actions.ts` 주석 참조).
*
* 기존 첨부는 **다운로드 링크로만** 보여준다. 시안(A_BOA_008_p)은 `pic_0001.jpg 88KB`처럼 파일명과
* 용량을 표기하지만, **백엔드에 atchFileId로 파일 정보를 조회하는 API가 없어** 저장된 첨부의
* 이름·크기를 알 방법이 없다(업로드·다운로드·삭제·이미지·스트림만 존재). 그 API가 생기면 여기에
* 파일명·용량 표기와 개별 삭제 버튼을 붙이면 된다.
*
* 새 파일을 고르면 기존 첨부는 교체된다(백엔드 필드가 `atchFileId` 하나뿐이다). 파일을 고르지
* 않으면 기존 첨부가 그대로 유지된다.
*/
const OVERSIZE_MESSAGE = `첨부파일은 ${Math.floor(
MAX_ATTACHMENT_BYTES / (1024 * 1024)
)}MB 이하만 올릴 수 있습니다.`;
const EXTENSION_MESSAGE = `${allowedExtensions(FILE_MODULE.board).join(', ')} 파일만 올릴 수 있습니다.`;
export function BoardAttachmentField({
label = '첨부파일',
attachmentId,
}: BoardAttachmentFieldProps) {
const [error, setError] = useState<string | undefined>(undefined);
return (
<label className={styles.field}>
<FoxFormLabel as="span">{label}</FoxFormLabel>
{/* 기존 첨부 id를 함께 제출해 "파일을 안 골랐으면 유지"를 서버가 판단할 수 있게 한다. */}
<input type="hidden" name="attachmentId" value={attachmentId ?? ''} />
{attachmentId ? (
<p className={styles.existing}>
기존 첨부파일{' '}
<a
href={`${ATTACHMENT_DOWNLOAD_PATH}?atchFileId=${encodeURIComponent(attachmentId)}`}
className={styles.download}
>
다운로드
</a>{' '}
— 새 파일을 선택하면 교체됩니다.
</p>
) : null}
{/* 이슈: 상한을 넘는 파일은 Server Action 본문이 잘려 500으로 끝나고 액션이 실행되지
않는다. 서버에서 잡을 수 없어 브라우저에서 미리 막는다. */}
<input
type="file"
name="attachment"
accept={fileModuleAccept(FILE_MODULE.board)}
className={styles.input}
onChange={(event) => {
const file = event.target.files?.[0];
if (!file) {
setError(undefined);
return;
}
if (file.size > MAX_ATTACHMENT_BYTES) {
event.target.value = '';
setError(OVERSIZE_MESSAGE);
return;
}
if (!isAllowedFileExtension(FILE_MODULE.board, file.name)) {
event.target.value = '';
setError(EXTENSION_MESSAGE);
return;
}
setError(undefined);
}}
/>
{error && <FoxHelperText type="danger" message={error} />}
</label>
);
}