feat: 이미지 등록 폼 작업
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<div className="file-uploader-preview">
|
||||
{files?.filter(validFileType).map(combineFileName).join(", ")}
|
||||
</div>
|
||||
{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 };
|
||||
Generated
+29
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user