From 1bf2919a09b1498660ec94c6ecdc8418dd999bce Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 2 May 2025 18:22:40 +0900 Subject: [PATCH] =?UTF-8?q?WIP:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=ED=8C=8C=EC=9D=BC=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attachment/api/attachment-service.ts | 4 + .../attachment/model/attachment-interface.ts | 1 + .../common/features/src/entities/tus/index.ts | 1 + .../features/src/entities/tus/lib/utils.ts | 60 +++++++++ .../notice/model/notice-table-columns.tsx | 3 +- .../src/pages/notice/ui/create-page.tsx | 7 +- .../src/pages/notice/ui/modify-page.tsx | 4 +- .../common/features/src/widgets/post/index.ts | 1 + .../post/ui/post-form.tsx} | 125 ++++++++++++++++-- 9 files changed, 185 insertions(+), 21 deletions(-) create mode 100644 apps/common/features/src/entities/tus/index.ts create mode 100644 apps/common/features/src/entities/tus/lib/utils.ts rename apps/common/features/src/{pages/notice/ui/notice-form.tsx => widgets/post/ui/post-form.tsx} (73%) diff --git a/apps/common/features/src/entities/attachment/api/attachment-service.ts b/apps/common/features/src/entities/attachment/api/attachment-service.ts index f956f6580..92800794a 100644 --- a/apps/common/features/src/entities/attachment/api/attachment-service.ts +++ b/apps/common/features/src/entities/attachment/api/attachment-service.ts @@ -42,6 +42,10 @@ export class AttachmentApiService implements AttachmentService { return URL.createObjectURL(blob); } + public async deleteAttachmentByAtflId(atflId: string): Promise { + await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`); + } + public async getImageUrlByAtflCd(atflCd: string): Promise { if (!atflCd) return ""; const response = await fetch( diff --git a/apps/common/features/src/entities/attachment/model/attachment-interface.ts b/apps/common/features/src/entities/attachment/model/attachment-interface.ts index 5133e4a72..f9dd31c16 100644 --- a/apps/common/features/src/entities/attachment/model/attachment-interface.ts +++ b/apps/common/features/src/entities/attachment/model/attachment-interface.ts @@ -36,6 +36,7 @@ export interface AttachmentService { getAttachmentsList: (params: AttachmentParams) => Promise; getAttachmentsInfo: (atflCd: string) => Promise; deleteAttachment: (atflId: string) => Promise; + deleteAttachmentByAtflId: (atflId: string) => Promise; deleteAttachmentByAtflCd: (atflCd: string) => Promise; getImageUrl: (atflId: string) => Promise; getImageUrlByAtflCd: (atflCd: string) => Promise; diff --git a/apps/common/features/src/entities/tus/index.ts b/apps/common/features/src/entities/tus/index.ts new file mode 100644 index 000000000..002cb2eda --- /dev/null +++ b/apps/common/features/src/entities/tus/index.ts @@ -0,0 +1 @@ +export { handleTusUpload } from "./lib/utils"; diff --git a/apps/common/features/src/entities/tus/lib/utils.ts b/apps/common/features/src/entities/tus/lib/utils.ts new file mode 100644 index 000000000..22747c26a --- /dev/null +++ b/apps/common/features/src/entities/tus/lib/utils.ts @@ -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); +}; diff --git a/apps/common/features/src/pages/notice/model/notice-table-columns.tsx b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx index f6036f050..d926b5f5d 100644 --- a/apps/common/features/src/pages/notice/model/notice-table-columns.tsx +++ b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx @@ -53,9 +53,8 @@ export const columns: ColumnDef[] = [ const AttachmentButton = ({ atflCd }: { atflCd: string }) => { if (!atflCd) return null; return ( - ); }; diff --git a/apps/common/features/src/pages/notice/ui/create-page.tsx b/apps/common/features/src/pages/notice/ui/create-page.tsx index e525a5949..b79d5235f 100644 --- a/apps/common/features/src/pages/notice/ui/create-page.tsx +++ b/apps/common/features/src/pages/notice/ui/create-page.tsx @@ -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() {

게시글 등록

- + ); diff --git a/apps/common/features/src/pages/notice/ui/modify-page.tsx b/apps/common/features/src/pages/notice/ui/modify-page.tsx index 590485f36..e769af78e 100644 --- a/apps/common/features/src/pages/notice/ui/modify-page.tsx +++ b/apps/common/features/src/pages/notice/ui/modify-page.tsx @@ -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 }) {

게시글 수정

- + ); diff --git a/apps/common/features/src/widgets/post/index.ts b/apps/common/features/src/widgets/post/index.ts index 9c833e568..743492614 100644 --- a/apps/common/features/src/widgets/post/index.ts +++ b/apps/common/features/src/widgets/post/index.ts @@ -1 +1,2 @@ export { Post } from "./ui/post"; +export { PostForm } from "./ui/post-form"; diff --git a/apps/common/features/src/pages/notice/ui/notice-form.tsx b/apps/common/features/src/widgets/post/ui/post-form.tsx similarity index 73% rename from apps/common/features/src/pages/notice/ui/notice-form.tsx rename to apps/common/features/src/widgets/post/ui/post-form.tsx index c52740f56..5c17a3cd3 100644 --- a/apps/common/features/src/pages/notice/ui/notice-form.tsx +++ b/apps/common/features/src/widgets/post/ui/post-form.tsx @@ -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 = { +type PostFormProps = { form: UseFormReturn; handleSave: (data: T) => Promise; }; -function NoticeForm({ +function PostForm({ form, handleSave, -}: NoticeFormProps) { +}: PostFormProps) { + const atflCd = form.watch("atflCd" as FieldPath); + const { userInfo } = useUserStore((state) => state); const [storedBbsInfo] = useLocalStorage("bbs-info", ""); const [bbsInfo, setBbsInfo] = useState(null); + const [attachments, setAttachments] = useState>( + {}, + ); + const [deleteAttachments, setDeleteAttachments] = useState([]); + + 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, 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, + ); + if (attachmentData) setAttachments(mapper(attachmentData)); + }, [attachmentData]); useEffect(() => { if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); }, [storedBbsInfo]); return ( - +
@@ -265,14 +330,48 @@ function NoticeForm({ 첨부파일 - } - render={() => ( - - - - )} + { + const files = e.target.files; + if (!files) return; + for (const file of files) { + handleTusUpload(file, { + handleResponse: handleTusResponse, + }); + } + }} + customPreview={ +
+ {Object.entries(attachments).map( + ([atflId, attachment]) => { + if (deleteAttachments.includes(atflId)) return null; + return ( +
+ {attachment.orgnlFileNm} + +
+ ); + }, + )} +
+ } />
@@ -315,4 +414,4 @@ const PeriodSelector = ({ disabled }: { disabled: boolean }) => { ); }; -export { NoticeForm }; +export { PostForm };