feat: 이미지 등록 폼 작업

This commit is contained in:
2025-04-02 13:22:11 +09:00
parent 3832adf3b0
commit 6297d528fb
9 changed files with 176 additions and 20 deletions
@@ -1,10 +1,10 @@
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { images } from "@common/assets";
import { LoginApi } from "@/entities/auth";
import "./login.css";
import { useRouter } from "next/navigation";
function LoginPage() {
const router = useRouter();
+61
View File
@@ -0,0 +1,61 @@
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) => void;
};
export const handleUpload = (files: FileList, options?: UploadOptions) => {
let metadata = {};
const file = files.item(0);
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);
},
onSuccess() {
options?.handleSuccess?.(upload);
},
});
upload.start();
options?.handleUploadStart?.(upload);
console.log("upload start >>>>>", upload);
};
+44 -7
View File
@@ -1,5 +1,6 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import type { UseFormReturn } from "react-hook-form";
import {
@@ -7,6 +8,7 @@ import {
FileUploader,
Input,
Label,
Progress,
RadioGroup,
RadioGroupItem,
Table,
@@ -16,8 +18,10 @@ import {
TableRow,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
import type { ImageFormData } from "../model/image-interface";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import type { ImageFormData } from "../model/image-interface";
import { handleUpload } from "../lib/utils";
import type { HttpResponse, Upload } from "tus-js-client";
type ImageFormProps = {
form: UseFormReturn<ImageFormData>;
@@ -29,7 +33,10 @@ type ImageFormProps = {
function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
const isModifyMode = !!imgSn;
const atflYn = form.watch("atflYn");
const router = useRouter();
const [upload, setUpload] = useState<Upload | null>(null);
const [progress, setProgress] = useState(0);
const onSubmit = async (data: ImageFormData) => {
await handleSave?.(data);
@@ -39,6 +46,17 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
await handleDelete?.();
};
const handleProgress = (percentage: string) => {
setProgress(Number(percentage));
};
const handleUploadResponse = (res: HttpResponse) => {
const fileInfo = res.getHeader("X-File-Info");
if (fileInfo === undefined || fileInfo === null) return;
form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
@@ -154,17 +172,36 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
<FormField
control={form.control}
name="atflCd"
render={({ field }) => (
<FormItem>
render={() => (
<FormItem className="flex flex-col">
<FormControl>
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
onUpload={(e) => {
const file = e.target.files?.[0];
if (file) {
field.onChange(file.name);
}
const files = e.target.files;
if (!files) return;
handleUpload(files, {
handleUploadStart: setUpload,
handleProgress,
handleSuccess: setUpload,
handleResponse: handleUploadResponse,
});
}}
customPreview={
<Label htmlFor="atflCd">
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
<p className="absolute left-0 top-0 p-2 text-base leading-none">
{(upload?.file as File)?.name}
</p>
<div></div>
<Progress
value={progress}
className="absolute bottom-0 left-0"
/>
</div>
</Label>
}
/>
</FormControl>
</FormItem>
+1
View File
@@ -35,6 +35,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
@@ -2,12 +2,12 @@
@layer components {
.file-uploader-container {
@apply flex w-full max-w-sm items-center gap-2;
@apply flex h-[3.2rem] w-full max-w-sm items-center gap-2;
}
.file-uploader-preview {
@apply h-12 w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
@apply h-full w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
}
.file-uploader-label {
@apply flex-shrink-0 cursor-pointer rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
@apply inline-flex h-full flex-shrink-0 cursor-pointer items-center rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
}
}
@@ -8,13 +8,15 @@ import { combineFileName, validFileType } from "./utils";
type FileUploaderProps = React.InputHTMLAttributes<HTMLInputElement> & {
id: string;
files?: ReadonlyArray<File>;
customPreview?: React.ReactNode;
onUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
export function FileUploader({
id,
className,
files,
customPreview,
className,
onUpload,
...props
}: FileUploaderProps) {
@@ -27,9 +29,11 @@ export function FileUploader({
{...props}
onChange={onUpload}
/>
{customPreview || (
<div className="file-uploader-preview">
{files?.filter(validFileType).map(combineFileName).join(", ")}
</div>
)}
<Label htmlFor={id} className="file-uploader-label">
파일 선택
</Label>
@@ -15,6 +15,7 @@ export * from "./layout";
export * from "./loading";
export * from "./page-title";
export * from "./popover";
export * from "./progress";
export * from "./radio-group";
export * from "./select";
export * from "./slider";
@@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "../../lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
+29 -5
View File
@@ -390,6 +390,9 @@ importers:
'@radix-ui/react-popover':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-radio-group':
specifier: ^1.2.2
version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
@@ -1623,7 +1626,7 @@ packages:
magic-string: 0.27.0
react-docgen-typescript: 2.2.2(typescript@5.6.3)
typescript: 5.6.3
vite: 6.2.0(@types/node@22.13.9)
vite: 6.2.0(@types/node@22.13.5)
dev: true
/@jridgewell/gen-mapping@0.3.8:
@@ -2295,6 +2298,27 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
peerDependencies:
'@types/react': ^18.3.18
'@types/react-dom': ^18.3.5
react: ^18.3.1
react-dom: ^18.3.1
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
peerDependencies:
@@ -3059,7 +3083,7 @@ packages:
browser-assert: 1.2.1
storybook: 8.6.0(prettier@3.5.2)
ts-dedent: 2.2.0
vite: 6.2.0(@types/node@22.13.9)
vite: 6.2.0(@types/node@22.13.5)
dev: true
/@storybook/components@8.6.0(storybook@8.6.0):
@@ -3191,7 +3215,7 @@ packages:
resolve: 1.22.10
storybook: 8.6.0(prettier@3.5.2)
tsconfig-paths: 4.2.0
vite: 6.2.0(@types/node@22.13.9)
vite: 6.2.0(@types/node@22.13.5)
transitivePeerDependencies:
- rollup
- supports-color
@@ -3637,7 +3661,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9)
'@types/babel__core': 7.20.5
react-refresh: 0.14.2
vite: 6.2.0(@types/node@22.13.9)
vite: 6.2.0(@types/node@22.13.5)
transitivePeerDependencies:
- supports-color
dev: true
@@ -8414,7 +8438,7 @@ packages:
debug: 4.4.0
globrex: 0.1.2
tsconfck: 3.1.5(typescript@5.6.3)
vite: 6.2.0(@types/node@22.13.9)
vite: 6.2.0(@types/node@22.13.5)
transitivePeerDependencies:
- supports-color
- typescript