File name
Commit message
Commit date
File name
Commit message
Commit date
09-16
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>
);
}