From 9b44458a12a8cb98a0c9920c6a553123482a169f Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Wed, 5 Mar 2025 14:51:39 +0900 Subject: [PATCH 1/5] =?UTF-8?q?search=5Fform=20=EB=B0=8F=20selectbox=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/search_form/index.tsx | 14 +++++- .../src/ui/select/index.tsx | 44 ++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/common/business_components/src/ui/search_form/index.tsx b/packages/common/business_components/src/ui/search_form/index.tsx index 457c22024..13391dc01 100644 --- a/packages/common/business_components/src/ui/search_form/index.tsx +++ b/packages/common/business_components/src/ui/search_form/index.tsx @@ -1,6 +1,18 @@ +/** + * 검색영역 스타일 + */ +const styles = { + divStyle : { + border: '1px solid black', + margin: '1rem', + display: 'flex', + borderRadius: '1rem' + } as React.CSSProperties +} + function SearchDiv ({children}:{ children: React.ReactNode; }) { return ( -
+
{children}
) diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index 1b473f4c3..3b48b6c3f 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -1,23 +1,43 @@ +import { Label } from "@common/components"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox"; +/** 차트 기본 스타일 */ +const styles = { + selectBoxDiv : { + marginLeft : '2rem', + display : 'flex' + } as React.CSSProperties, + selectStyle : { + marginLeft: '1rem', + width:'8rem' + } as React.CSSProperties, + labelStyle : { + margin:'0.8rem 0.8rem 0.8rem 0.8rem', + fontSize: '10px', + } as React.CSSProperties +} + interface SelectBox1 { text:string, val:string } -function SelectBox(props: { selectArray: SelectBox1[]}){ +function SelectBox(props: {label : string, selectArray: SelectBox1[]}){ return ( - +
+ + +
) } -export { SelectBox }; +export { SelectBox }; \ No newline at end of file From 6fe828d77fdddf8af311c87f5ebdc23911552167 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Wed, 5 Mar 2025 18:17:36 +0900 Subject: [PATCH 2/5] =?UTF-8?q?placeHolder,=20label=20undefined=EC=8B=9C?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=EC=B6=94=EA=B0=80=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/select/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/common/business_components/src/ui/select/index.tsx b/packages/common/business_components/src/ui/select/index.tsx index 3b48b6c3f..72f304caa 100644 --- a/packages/common/business_components/src/ui/select/index.tsx +++ b/packages/common/business_components/src/ui/select/index.tsx @@ -23,12 +23,14 @@ interface SelectBox1 { } function SelectBox(props: {label : string, selectArray: SelectBox1[]}){ + const placeHolder = props.selectArray.length > 0 ? props.selectArray[0].text : ""; + return (
- + {props.label && } - - - - )} - /> -
- -
+ // 2. Define a submit handler. + function onSubmit(values: z.infer) { + // Do something with the form values. + // ✅ This will be type-safe and validated. + console.log(values) + } -
- -
- - ( - - 계통도 - - - {/* (null); + + return ( + <> +
+
+ + {/*
*/} +
+
+
+
+
+
+
+ +
+
+ +
+ +
+ + ( + + 계통도 + + + {/* */} - - - - )} - /> -
- -
-
- - ( - - 평면도 - - - {/* - -
- -

- Click to upload -   or drag and drop -

-

- SVG, PNG, JPG or GIF -

-
-
- - {files && - files.length > 0 && - files.map((file, i) => ( - - - {file.name} - - ))} - -
*/} -
- -
- )} - /> -
- -
+ + + + )} + /> +
+
+
- -
-

○ 재이용시설 운영정보

- -
-
- -
-
-

○ 재이용수 공급 가능량

-
- -
- - ( - - 시설용량(m³/일) - - - - - - )} - /> -
- -
- - ( - - 하수처리량(m³/일) - - - - - - )} - /> -
- -
- - ( - - 재이용량(m³/일) - - - - - - )} - /> -
-
- + ( + + 평면도 + ( - 공급여유량(m³/일) + Select File - + + Select a file to upload. )} /> -
-
-
- - + {/* + +
+ +

+ Click to upload +   or drag and drop +

+

+ SVG, PNG, JPG or GIF +

+
+
+ + {files && + files.length > 0 && + files.map((file, i) => ( + + + {file.name} + + ))} + +
*/} + + + + )} + /> +
+ +
+ + + +
+

○ 재이용시설 운영정보

+ +
+
+ +
+
+

○ 재이용수 공급 가능량

+
+ +
+ + ( + + 시설용량(m³/일) + + + + + + )} + /> +
+ +
+ + ( + + 하수처리량(m³/일) + + + + + + )} + /> +
+ +
+ + ( + + 재이용량(m³/일) + + + + + + )} + /> +
+
+ + ( + + 공급여유량(m³/일) + + + + + + )} + /> +
+
+
- -
- {/* 차수별 등록 현호아 폼 */} -
-
- +
+
+ {/* 차수별 등록 현호아 폼 */} +
+
+ -
-
-

○ 하수처리수 재이용 시설 현황

-
-
- ( - - 등록 차수 - - - - )} - /> - -
- - - - -
-
- +
+
+

○ 하수처리수 재이용 시설 현황

+
+
+ ( + + 등록 차수 + + + + )} + /> + +
- - - ); + + +
+ + + ); } \ No newline at end of file From 432579de83b9cb3611acee49764739e5badd480c Mon Sep 17 00:00:00 2001 From: truthwing Date: Thu, 6 Mar 2025 17:51:26 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20conponent=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/package.json | 2 + .../src/ui/file-upload/index.tsx | 49 +++ .../src/ui/file-upload/lib/file_upload.tsx | 364 ++++++++++++++++++ .../business_components/src/ui/index.ts | 5 +- 4 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 packages/common/business_components/src/ui/file-upload/index.tsx create mode 100644 packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx diff --git a/packages/common/business_components/package.json b/packages/common/business_components/package.json index 551de2ac5..9618ff295 100644 --- a/packages/common/business_components/package.json +++ b/packages/common/business_components/package.json @@ -34,8 +34,10 @@ "clsx": "^2.1.1", "lucide-react": "^0.474.0", "path": "^0.12.7", + "react-dropzone": "^14.3.8", "react-hook-form": "^7.54.2", "recharts": "^2.15.1", + "sonner": "^2.0.1", "tailwind-merge": "^3.0.1", "zod": "^3.24.2" }, diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx new file mode 100644 index 000000000..13126692d --- /dev/null +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -0,0 +1,49 @@ +"use client"; +import { CloudUpload, Paperclip } from "lucide-react"; +import React from "react"; +import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload"; +const dropZoneConfig = { + maxFiles: 5, + maxSize: 1024 * 1024 * 4, + multiple: true, + }; +export const FileUpload = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes & { files: File[]|null, setFiles: (files: File[]|null) => void , fileTypes: string|undefined} +>(({ ...props }) => { + return ( + + +
+ +

+ Click to upload +   or drag and drop +

+

+ {!props?.fileTypes?"SVG, PNG, JPG, GIF":props?.fileTypes} +

+
+
+ + {props.files && + props.files.length > 0 && + props.files.map((file:File, i:number) => ( + + + {file.name} + + ))} + +
+ ) +}) \ No newline at end of file diff --git a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx new file mode 100644 index 000000000..3db4bcceb --- /dev/null +++ b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx @@ -0,0 +1,364 @@ +"use client"; + +import { Input, buttonVariants } from "@common/components"; +import { Trash2 as RemoveIcon } from "lucide-react"; +import { + type Dispatch, + type SetStateAction, + createContext, + forwardRef, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + type DropzoneOptions, + type DropzoneState, + type FileRejection, + useDropzone, +} from "react-dropzone"; +import { toast } from "sonner"; +import { cn } from "../../../lib/utils"; + +type DirectionOptions = "rtl" | "ltr" | undefined; + +type FileUploaderContextType = { + dropzoneState: DropzoneState; + isLOF: boolean; + isFileTooBig: boolean; + removeFileFromSet: (index: number) => void; + activeIndex: number; + setActiveIndex: Dispatch>; + orientation: "horizontal" | "vertical"; + direction: DirectionOptions; +}; + +const FileUploaderContext = createContext(null); + +export const useFileUpload = () => { + const context = useContext(FileUploaderContext); + if (!context) { + throw new Error("useFileUpload must be used within a FileUploaderProvider"); + } + return context; +}; + +type FileUploaderProps = { + value: File[] | null; + reSelect?: boolean; + onValueChange: (value: File[] | null) => void; + dropzoneOptions: DropzoneOptions; + orientation?: "horizontal" | "vertical"; +}; + +/** + * File upload Docs: {@link: https://localhost:3000/docs/file-upload} + */ + +export const FileUploader = forwardRef< + HTMLDivElement, + FileUploaderProps & React.HTMLAttributes & {acceptName?: string|undefined} +>( + ( + { + className, + dropzoneOptions, + value, + onValueChange, + reSelect, + orientation = "vertical", + children, + dir, + acceptName, + ...props + }, + ref, + ) => { + const [isFileTooBig, setIsFileTooBig] = useState(false); + const [isLOF, setIsLOF] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + const { + accept = { + "image/*": acceptName?acceptName.split(",").map(extend=>("."+extend)):[".jpg", ".jpeg", ".png", ".gif"], + }, + maxFiles = 1, + maxSize = 4 * 1024 * 1024, + multiple = true, + } = dropzoneOptions; + + const reSelectAll = maxFiles === 1 ? true : reSelect; + const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr"; + + const removeFileFromSet = useCallback( + (i: number) => { + if (!value) return; + const newFiles = value.filter((_, index) => index !== i); + onValueChange(newFiles); + }, + [value, onValueChange], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!value) return; + + const moveNext = () => { + const nextIndex = activeIndex + 1; + setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex); + }; + + const movePrev = () => { + const nextIndex = activeIndex - 1; + setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex); + }; + + const prevKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowLeft" + : "ArrowRight" + : "ArrowUp"; + + const nextKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowRight" + : "ArrowLeft" + : "ArrowDown"; + + if (e.key === nextKey) { + moveNext(); + } else if (e.key === prevKey) { + movePrev(); + } else if (e.key === "Enter" || e.key === "Space") { + if (activeIndex === -1) { + dropzoneState.inputRef.current?.click(); + } + } else if (e.key === "Delete" || e.key === "Backspace") { + if (activeIndex !== -1) { + removeFileFromSet(activeIndex); + if (value.length - 1 === 0) { + setActiveIndex(-1); + return; + } + movePrev(); + } + } else if (e.key === "Escape") { + setActiveIndex(-1); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [value, activeIndex, removeFileFromSet], + ); + + const onDrop = useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + const files = acceptedFiles; + + if (!files) { + toast.error("file error , probably too big"); + return; + } + + const newValues: File[] = value ? [...value] : []; + + if (reSelectAll) { + newValues.splice(0, newValues.length); + } + + files.forEach((file) => { + if (newValues.length < maxFiles) { + newValues.push(file); + } + }); + + onValueChange(newValues); + + if (rejectedFiles?.length > 0) { + for (let i = 0; i < rejectedFiles?.length; i++) { + if (rejectedFiles[i]?.errors[0]?.code === "file-too-large") { + toast.error( + `File is too large. Max size is ${maxSize / 1024 / 1024}MB`, + ); + break; + } + if (rejectedFiles[i]?.errors[0]?.message) { + toast.error(rejectedFiles[i]?.errors[0]?.message); + break; + } + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [reSelectAll, value], + ); + + useEffect(() => { + if (!value) return; + if (value.length === maxFiles) { + setIsLOF(true); + return; + } + setIsLOF(false); + }, [value, maxFiles]); + + const opts = dropzoneOptions + ? dropzoneOptions + : { accept, maxFiles, maxSize, multiple }; + + const dropzoneState = useDropzone({ + ...opts, + onDrop, + onDropRejected: () => setIsFileTooBig(true), + onDropAccepted: () => setIsFileTooBig(false), + }); + + return ( + +
0, + }, + )} + dir={dir} + {...props} + > + {children} +
+
+ ); + }, +); + +FileUploader.displayName = "FileUploader"; + +export const FileUploaderContent = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ children, className, ...props }, ref) => { + const { orientation } = useFileUpload(); + const containerRef = useRef(null); + + return ( +
+
+ {children} +
+
+ ); +}); + +FileUploaderContent.displayName = "FileUploaderContent"; + +export const FileUploaderItem = forwardRef< + HTMLDivElement, + { index: number } & React.HTMLAttributes +>(({ className, index, children, ...props }, ref) => { + const { removeFileFromSet, activeIndex, direction } = useFileUpload(); + const isSelected = index === activeIndex; + return ( +
+
+ {children} +
+ +
+ ); +}); + +FileUploaderItem.displayName = "FileUploaderItem"; + +export const FileInput = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + const { dropzoneState, isFileTooBig, isLOF } = useFileUpload(); + const rootProps = isLOF ? {} : dropzoneState.getRootProps(); + return ( +
+
+ {children} +
+ +
+ ); +}); + +FileInput.displayName = "FileInput"; diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index ef8fa6f8c..1876f2b50 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -1,5 +1,6 @@ export * from "./chart"; -export * from "./table"; +export * from "./file-upload"; export * from "./form"; export * from "./search_form"; -export * from "./select"; \ No newline at end of file +export * from "./select"; +export * from "./table"; From dae9374b285e7edd6fdcb2dcf5a1dc071050c913 Mon Sep 17 00:00:00 2001 From: truthwing Date: Thu, 6 Mar 2025 18:01:12 +0900 Subject: [PATCH 5/5] =?UTF-8?q?FileUpload=20=ED=83=80=EC=9E=85=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EB=A1=9C=20=EC=88=98=EC=A0=95.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/file-upload/index.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx index 13126692d..3d581ed05 100644 --- a/packages/common/business_components/src/ui/file-upload/index.tsx +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -1,16 +1,13 @@ "use client"; import { CloudUpload, Paperclip } from "lucide-react"; -import React from "react"; import { FileInput, FileUploader, FileUploaderContent, FileUploaderItem } from "./lib/file_upload"; const dropZoneConfig = { maxFiles: 5, maxSize: 1024 * 1024 * 4, multiple: true, }; -export const FileUpload = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes & { files: File[]|null, setFiles: (files: File[]|null) => void , fileTypes: string|undefined} ->(({ ...props }) => { +export const FileUpload = +(({ ...props }) => { return (