WIP: 게시글 첨부파일 작업중
This commit is contained in:
@@ -42,6 +42,10 @@ export class AttachmentApiService implements AttachmentService {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async deleteAttachmentByAtflId(atflId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
|
||||
}
|
||||
|
||||
public async getImageUrlByAtflCd(atflCd: string): Promise<string> {
|
||||
if (!atflCd) return "";
|
||||
const response = await fetch(
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface AttachmentService {
|
||||
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
|
||||
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
|
||||
deleteAttachment: (atflId: string) => Promise<void>;
|
||||
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
|
||||
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
|
||||
getImageUrl: (atflId: string) => Promise<string>;
|
||||
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { handleTusUpload } from "./lib/utils";
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Upload, type HttpResponse } from "tus-js-client";
|
||||
import { getCookie, setCookie } from "@/shared/lib";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
|
||||
type UploadOptions = {
|
||||
handleUploadStart?: (upload: Upload) => void;
|
||||
handleProgress?: (percentage: string) => void;
|
||||
handleSuccess?: (upload: Upload) => void;
|
||||
handleResponse?: (res: HttpResponse, upload: Upload) => void;
|
||||
};
|
||||
|
||||
export const handleTusUpload = (file: File, options?: UploadOptions) => {
|
||||
let metadata = {};
|
||||
|
||||
if (!file) return;
|
||||
metadata = {
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
};
|
||||
|
||||
const atflCd = getCookie("atfl-cd-key");
|
||||
if (atflCd) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
atflCd,
|
||||
};
|
||||
}
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024,
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
metadata,
|
||||
onError(error) {
|
||||
console.error(`Failed because: ${error}`);
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
options?.handleProgress?.(percentage);
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? "");
|
||||
options?.handleResponse?.(res, upload);
|
||||
},
|
||||
onSuccess() {
|
||||
options?.handleSuccess?.(upload);
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
options?.handleUploadStart?.(upload);
|
||||
|
||||
console.log("upload start >>>>>", upload);
|
||||
};
|
||||
@@ -53,9 +53,8 @@ export const columns: ColumnDef<Post>[] = [
|
||||
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
|
||||
if (!atflCd) return null;
|
||||
return (
|
||||
<Button size="sm" color="white" className="flex items-center gap-2">
|
||||
<Button size="icon" color="white">
|
||||
<i className="diveicon di-download" />
|
||||
다운로드
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { PostApi } from "@/entities/post";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { NoticeForm } from "./notice-form";
|
||||
import { NoticeLayout } from "./notice-layout";
|
||||
|
||||
function CreatePage() {
|
||||
@@ -41,7 +40,7 @@ function CreatePage() {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시글 등록</h3>
|
||||
</div>
|
||||
<NoticeForm form={form} handleSave={createPost} />
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
</NoticeLayout>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { NoticeForm } from "./notice-form";
|
||||
import { NoticeLayout } from "./notice-layout";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
@@ -40,7 +40,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시글 수정</h3>
|
||||
</div>
|
||||
<NoticeForm form={form} handleSave={updatePost} />
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
</NoticeLayout>
|
||||
);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { Post } from "./ui/post";
|
||||
export { PostForm } from "./ui/post-form";
|
||||
|
||||
+112
-13
@@ -1,5 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import type { UseFormReturn, FieldPath } from "react-hook-form";
|
||||
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
@@ -14,34 +16,97 @@ import {
|
||||
TableRow,
|
||||
Textarea,
|
||||
DateRangePicker,
|
||||
FileUploader,
|
||||
Button,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { Board } from "@/entities/board";
|
||||
import { handleTusUpload } from "@/entities/tus";
|
||||
import { AttachmentApi, type Attachment } from "@/entities/attachment";
|
||||
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
type NoticeFormProps<T extends PostFormData | PostUpdateFormData> = {
|
||||
type PostFormProps<T extends PostFormData | PostUpdateFormData> = {
|
||||
form: UseFormReturn<T>;
|
||||
handleSave: (data: T) => Promise<Post>;
|
||||
};
|
||||
|
||||
function NoticeForm<T extends PostFormData | PostUpdateFormData>({
|
||||
function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
form,
|
||||
handleSave,
|
||||
}: NoticeFormProps<T>) {
|
||||
}: PostFormProps<T>) {
|
||||
const atflCd = form.watch("atflCd" as FieldPath<T>);
|
||||
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
|
||||
|
||||
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
|
||||
const [attachments, setAttachments] = useState<Record<string, Attachment>>(
|
||||
{},
|
||||
);
|
||||
const [deleteAttachments, setDeleteAttachments] = useState<string[]>([]);
|
||||
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd as string),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleTusResponse = (res: HttpResponse, upload: Upload) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (
|
||||
typeof fileInfo !== "string" ||
|
||||
fileInfo === undefined ||
|
||||
fileInfo === null
|
||||
)
|
||||
return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd" as FieldPath<T>, atflCd);
|
||||
if (atflId) {
|
||||
setAttachments((prev) => ({
|
||||
...prev,
|
||||
[atflId]: {
|
||||
atflCd,
|
||||
atflId,
|
||||
orgnlFileNm: (upload.file as File).name,
|
||||
atflFlieSz: (upload.file as File).size,
|
||||
atflType: (upload.file as File).type,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const saveInterceptors = async (data: T) => {
|
||||
if (deleteAttachments.length > 0) {
|
||||
await Promise.all(
|
||||
deleteAttachments.map(AttachmentApi.deleteAttachmentByAtflId),
|
||||
);
|
||||
}
|
||||
handleSave(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const mapper = (data: Attachment[]) =>
|
||||
data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.atflId] = curr;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Attachment>,
|
||||
);
|
||||
if (attachmentData) setAttachments(mapper(attachmentData));
|
||||
}, [attachmentData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
|
||||
}, [storedBbsInfo]);
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} handleSave={handleSave}>
|
||||
<FormWrapper form={form} handleSave={saveInterceptors}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
@@ -265,14 +330,48 @@ function NoticeForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"atflCd" as FieldPath<T>}
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormControl></FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
multiple
|
||||
className="flex !h-auto max-w-full flex-row-reverse gap-4 [&>label]:h-[3.2rem]"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
for (const file of files) {
|
||||
handleTusUpload(file, {
|
||||
handleResponse: handleTusResponse,
|
||||
});
|
||||
}
|
||||
}}
|
||||
customPreview={
|
||||
<div className="flex w-full gap-4">
|
||||
{Object.entries(attachments).map(
|
||||
([atflId, attachment]) => {
|
||||
if (deleteAttachments.includes(atflId)) return null;
|
||||
return (
|
||||
<div
|
||||
key={atflId}
|
||||
className="flex h-[3.2rem] items-center gap-2 rounded-sm border border-border bg-background px-3 py-2 text-foreground"
|
||||
>
|
||||
<span>{attachment.orgnlFileNm}</span>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex size-7 items-center justify-center rounded-full p-0"
|
||||
onClick={() =>
|
||||
setDeleteAttachments((prev) => [
|
||||
...prev,
|
||||
atflId,
|
||||
])
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-x text-xs" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -315,4 +414,4 @@ const PeriodSelector = ({ disabled }: { disabled: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { NoticeForm };
|
||||
export { PostForm };
|
||||
Reference in New Issue
Block a user