Skip to content

Commit

Permalink
feat: 이미지 업로드 기능 구현 및 컨텐츠 등록기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
JoStar33 committed Aug 20, 2024
1 parent 7c7d958 commit 63d757b
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 14 deletions.
21 changes: 17 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/react-router-dom": "^5.3.3",
"axios": "^1.6.8",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"dayjs": "^1.11.12",
"framer-motion": "^11.0.23",
"pretendard": "^1.3.9",
"react": "^18.2.0",
Expand All @@ -30,6 +30,7 @@
"styled-components": "^6.1.8",
"styled-reset": "^4.5.2",
"use-mask-input": "^3.3.7",
"yet-another-react-lightbox": "^3.21.4",
"yup": "^1.4.0"
},
"devDependencies": {
Expand Down
Binary file added public/images/empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions src/components/contents/FloatWriteButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import FloatButtonWrapper from '@/components/common/FloatButtonWrapper';
import { FaPen } from 'react-icons/fa';

export default function FloatWriteButton() {
interface IProps {
handleRouteWrite: () => void;
}

export default function FloatWriteButton({ handleRouteWrite }: IProps) {
return (
<FloatButtonWrapper>
<FloatButtonWrapper onClick={handleRouteWrite}>
<FaPen size={25} />
</FloatButtonWrapper>
);
Expand Down
9 changes: 8 additions & 1 deletion src/components/contents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import { IContentsListResponse } from '@/types/contents';
import styled from 'styled-components';
import ContentCard from './ContentCard';
import FloatWriteButton from './FloatWriteButton';
import { useNavigate } from 'react-router-dom';
import routerPath from '@/constants/routerPath';

interface IProps {
data: IContentsListResponse;
}

export default function Contents({ data }: IProps) {
const navigate = useNavigate();
const handleRouteWrite = () => {
navigate(routerPath.CONTENTS_WRITE);
};

return (
<>
<FloatWriteButton />
<FloatWriteButton handleRouteWrite={handleRouteWrite} />
<S.Contents>
{data.value.map((element) => (
<ContentCard element={element} />
Expand Down
25 changes: 25 additions & 0 deletions src/components/contents/write/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Button from '@/components/common/Button';
import Form from '@/components/hookForm';
import { IContentsRegisterForm } from '@/types/contents';
import { SubmitHandler, useFormContext } from 'react-hook-form';
import styled from 'styled-components';

interface IProps {
onSubmit: SubmitHandler<IContentsRegisterForm>;
}

export default function ContentsWrite({ onSubmit }: IProps) {
const { handleSubmit } = useFormContext<IContentsRegisterForm>();
return (
<S.ContentsWrite onSubmit={handleSubmit(onSubmit)}>
<Form.FileDrop<IContentsRegisterForm> name="image" />
<Form.InputA<IContentsRegisterForm> name="title" />
<Form.InputA<IContentsRegisterForm> name="description" />
<Button name="positive">등록</Button>
</S.ContentsWrite>
);
}

const S = {
ContentsWrite: styled.form``,
};
249 changes: 249 additions & 0 deletions src/components/hookForm/FileDrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import React from 'react';
import styled from 'styled-components';
import { motion } from 'framer-motion';
import { FaRegTrashAlt } from 'react-icons/fa';
import { useFormContext, Path, FieldValues, PathValue } from 'react-hook-form';
import { isFile } from '@/utils/isFile';
import { flexCenter } from '@/styles/Common';
import ErrorText from './ErrorText';
import Lightbox from 'yet-another-react-lightbox';

import 'yet-another-react-lightbox/styles.css';

interface IProps<T> {
name: Path<T>;
acceptedSize?: string;
boxSize?: number;
disabled?: boolean;
guidText?: string;
}

type TPreviewFiles = {
name: string;
src: string;
};

export default function ImageDrop<T extends FieldValues>({ name, boxSize = 150, disabled, acceptedSize, guidText }: IProps<T>) {
const {
register,
watch,
setValue,
clearErrors,
formState: { errors },
} = useFormContext<T>();
const [previewFiles, setPreviewFiles] = React.useState<TPreviewFiles[] | []>([]);
const inputFileRef = React.useRef<HTMLLabelElement>(null);
const [isDragActive, setIsDragActive] = React.useState(false);
const [open, setOpen] = React.useState(false);

const files: File[] = watch(name);

const handleDragStart = () => setIsDragActive(true);

const handleDragEnd = () => setIsDragActive(false);

const handleDragOver: React.DragEventHandler<HTMLLabelElement> = (event) => {
event.preventDefault();
};

const handleUploadImages = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
if (!event.target.files) return;
onDrop(event.target.files);
};

const handleDrop: React.DragEventHandler<HTMLLabelElement> = (event) => {
event.preventDefault();
if (!event.dataTransfer) return;
onDrop(event.dataTransfer.files);
};

const handleOpenLightbox = () => {
setOpen(true);
};

const handleCloseLightbox = () => {
setOpen(false);
};

const onDrop = React.useCallback(
(fileList: FileList) => {
const acceptedFiles = Array.from(fileList);
if (acceptedFiles.length === 0) return;
const file = acceptedFiles[0];

if (file.size > 2000000) return alert('2MB이하의 파일만 업로드가능합니다.');
if (!acceptedSize) return setValue(name, [file] as PathValue<T, Path<T>>);

const [acceptedWidth, acceptedHeight] = acceptedSize.split('x');
const image = document.createElement('img') as HTMLImageElement;
image.src = URL.createObjectURL(file);
image.onload = () => {
const width = image.width;
const height = image.height;

if (width !== Number(acceptedWidth) || height !== Number(acceptedHeight)) {
alert(`허용된 이미지 크기는 ${acceptedWidth}x${acceptedHeight} 입니다.`);
return;
}

setValue(name, [file] as PathValue<T, Path<T>>);
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[files, name, setValue],
);

const handleRemoveFile = (targetFileName: string) => {
if (Array.isArray(files) && files.length !== 0) {
const filterFiles = files.filter((file) => file.name !== targetFileName);
setValue(name, filterFiles as PathValue<T, Path<T>>);
}
};

React.useEffect(
function observeFileChange() {
if (!Array.isArray(files)) return;
const result = files.map((image, index) => {
if (isFile(image)) {
const isImage = image as File;
return { name: isImage.name.normalize('NFC'), src: window.URL.createObjectURL(isImage) };
}
return { name: String(index), src: image as string };
});
setPreviewFiles(result);
clearErrors(name);
},
[clearErrors, name, files],
);

React.useEffect(
function focusOnInput() {
if (!errors[name] || !inputFileRef.current) return;
inputFileRef.current.focus();
},
[errors, name],
);

return (
<S.ImageDrop boxSize={boxSize}>
<Lightbox open={open} close={handleCloseLightbox} slides={previewFiles} />

{!disabled && (
<div className="image-drop__wrapper">
<StyledImageArea
htmlFor={name}
boxSize={boxSize}
ref={inputFileRef}
onDragEnter={handleDragStart}
onDragOver={handleDragOver} // dragover 핸들러 추가
onDragLeave={handleDragEnd}
onChange={handleUploadImages as unknown as React.FormEventHandler<HTMLLabelElement>}
onDrop={handleDrop}
>
<input id={name} type="file" accept="image/*" {...register(name)} />
{isDragActive ? <motion.div animate={{ scale: 1.1 }} className="drop-in"></motion.div> : <div className="drop-in"></div>}
</StyledImageArea>
<p>{guidText}</p>
{errors[name] && <ErrorText errors={errors} name={name} margin="0 0 5px 0" />}
</div>
)}

{previewFiles.length !== 0 && (
<ul className="list">
{previewFiles.map((item) => (
<li className="list__item" key={item.name}>
<img
className="list__item--image"
src={name === 'logoImage' && !item.src ? '/images/empty.png' : item.src}
width={boxSize}
height={boxSize}
alt="컨텐츠 이미지"
onClick={handleOpenLightbox}
/>
{!disabled && <FaRegTrashAlt cursor="pointer" className="list__item--remove" onClick={() => handleRemoveFile(item.name)} />}
</li>
))}
</ul>
)}
{previewFiles.length === 0 && disabled && (
<div className="list__item--empty">
<p>비어있음</p>
</div>
)}
</S.ImageDrop>
);
}

const StyledImageArea = styled.label<{ boxSize: number }>`
border: 2px dashed ${(props) => props.theme.colors.deepSkyblue};
height: ${(props) => props.boxSize}px;
width: ${(props) => props.boxSize}px;
min-width: ${(props) => props.boxSize}px;
cursor: pointer;
font-size: 0;
margin-right: 15px;
margin-bottom: 15px;
${flexCenter}
:hover {
transform: scale(1.01);
}
.drop-in {
padding: 15px;
height: 100%;
}
input {
display: none;
}
`;

const S = {
ImageDrop: styled.div<{ boxSize: number }>`
width: 100%;
display: flex;
font-size: 0;
overflow-x: auto;
overflow-y: hidden;
.image-drop {
&__wrapper {
display: flex;
flex-direction: column;
}
}
.list {
&__item {
position: relative;
width: ${(props) => props.boxSize}px;
height: ${(props) => props.boxSize}px;
&--image {
object-fit: contain;
font-size: 0;
border: 1px solid #999;
cursor: pointer;
}
&--empty {
width: ${(props) => props.boxSize}px;
height: ${(props) => props.boxSize}px;
border: 1px solid #999;
${flexCenter}
p {
font-size: 15px;
}
}
&--remove {
position: absolute;
top: 5px;
right: 5px;
fill: #fff;
width: 18px;
height: 18px;
padding: 5px;
background-color: gray;
border-radius: 50%;
width: 30px;
height: 30px;
}
}
}
`,
};
6 changes: 4 additions & 2 deletions src/components/hookForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import ErrorText from '@/components/hookForm/ErrorText';
import InputA from '@/components/hookForm/InputA';
import RadioButton from './RadioButton';
import CheckBoxYN from './CheckBoxYN';
import RadioButton from '@/components/hookForm/RadioButton';
import CheckBoxYN from '@/components/hookForm/CheckBoxYN';
import FileDrop from '@/components/hookForm/FileDrop';

const Form = {
InputA,
ErrorText,
RadioButton,
CheckBoxYN,
FileDrop,
};

export default Form;
Loading

0 comments on commit 63d757b

Please sign in to comment.