From f9081f49ccb0df45d9c80179530130b752d8aebf Mon Sep 17 00:00:00 2001 From: BY-juun Date: Wed, 29 Nov 2023 21:25:39 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20image=20upload=20=ED=9B=85=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/Hooks/useImageUpload.ts | 61 ++++++++++++++ client/components/shared/Form/Editor.tsx | 40 +++------- .../components/shared/Form/ImageUploader.tsx | 33 ++++---- .../components/shared/Form/styles.module.scss | 22 +++++ .../shared/ImageUploader/ImageUploader.tsx | 80 ------------------- .../components/shared/ImageUploader/index.ts | 1 - .../shared/ImageUploader/styles.module.scss | 21 ----- server/build/database/models/Posts.js | 2 +- server/src/database/models/Posts.ts | 2 +- 9 files changed, 114 insertions(+), 148 deletions(-) create mode 100644 client/Hooks/useImageUpload.ts delete mode 100644 client/components/shared/ImageUploader/ImageUploader.tsx delete mode 100644 client/components/shared/ImageUploader/index.ts delete mode 100644 client/components/shared/ImageUploader/styles.module.scss diff --git a/client/Hooks/useImageUpload.ts b/client/Hooks/useImageUpload.ts new file mode 100644 index 00000000..3809f42c --- /dev/null +++ b/client/Hooks/useImageUpload.ts @@ -0,0 +1,61 @@ +import { ServerURL } from "@constants"; +import { useCallback, useState } from "react"; + +interface UploadImgResponse { + url: string; + uploaded: boolean; +} + +interface Params { + onImageUploadSuccess?: (params: UploadImgResponse) => void; +} + +const useImageUpload = (params?: Params) => { + const [uploadedImgSrc, setUploadedImgSrc] = useState(null); + + const handleClickUploadButton = useCallback(() => { + const inputElem = createInputElement(); + + document.body.appendChild(inputElem); + + inputElem.click(); + + inputElem.onchange = async () => { + const file = inputElem.files; + + if (!file) { + return; + } + + const { url, uploaded } = await uploadImg(file[0]); + params?.onImageUploadSuccess?.({ url, uploaded }); + setUploadedImgSrc(url); + document.body.removeChild(inputElem); + }; + }, [params]); + + return { uploadedImgSrc, setUploadedImgSrc, handleClickUploadButton }; +}; + +const uploadImg = async (img: File) => { + const formData = new FormData(); + formData.append("img", img); + + const response = await fetch(`${ServerURL}/uploads`, { + method: "post", + body: formData, + }); + + return (await response.json()) as UploadImgResponse; +}; + +const createInputElement = () => { + const inputElem = document.createElement("input"); + inputElem.setAttribute("type", "file"); + inputElem.setAttribute("accept", "image/*"); + inputElem.style.display = "none"; + document.body.appendChild(inputElem); + return inputElem; +}; + +export default useImageUpload; diff --git a/client/components/shared/Form/Editor.tsx b/client/components/shared/Form/Editor.tsx index e5b904f3..2d67dce4 100644 --- a/client/components/shared/Form/Editor.tsx +++ b/client/components/shared/Form/Editor.tsx @@ -1,4 +1,4 @@ -import React, { type LegacyRef, useMemo, useRef } from "react"; +import React, { type LegacyRef, useMemo, useRef, useCallback } from "react"; import dynamic from "next/dynamic"; import styles from "./styles.module.scss"; import "react-quill/dist/quill.snow.css"; @@ -7,6 +7,7 @@ import ReactQuill from "react-quill"; import Script from "next/script"; import "highlight.js/styles/atom-one-dark.css"; import { ServerURL } from "@constants"; +import useImageUpload from "@hooks/useImageUpload"; interface EditorProps { forwardedRef: LegacyRef | undefined; @@ -34,33 +35,17 @@ const Editor = ({ }: Pick) => { const QuillRef = useRef(null); - const imageHandler = () => { - const input = document.createElement("input"); - input.setAttribute("type", "file"); - input.setAttribute("accept", "image/*"); - document.body.appendChild(input); - input.style.display = "none"; - input.click(); - input.onchange = async () => { - const file = input.files; - if (file !== null && QuillRef.current) { - const formData = new FormData(); - formData.append("img", file[0]); + const onImageUploadSuccess = useCallback(({ url }: { url: string }) => { + if (!QuillRef.current) { + return; + } - const response = await fetch(`${ServerURL}/uploads`, { - method: "post", - body: formData, - }); + const editor = QuillRef.current.getEditor(); + const range = editor.getSelection(); + editor.insertEmbed(Number(range?.index), "image", url); + }, []); - const data: { url: string; uploaded: boolean } = await response.json(); - - const img_url = data.url; - const editor = QuillRef.current.getEditor(); - const range = editor.getSelection(); - editor.insertEmbed(Number(range?.index), "image", img_url); - } - }; - }; + const { handleClickUploadButton } = useImageUpload({ onImageUploadSuccess }); const modules = useMemo( () => ({ @@ -69,13 +54,14 @@ const Editor = ({ }, toolbar: { container: containerConfig, - handlers: { image: imageHandler }, + handlers: { image: handleClickUploadButton }, clipboard: { // toggle to add extra line breaks when pasting HTML: matchVisual: false, }, }, }), + // eslint-disable-next-line react-hooks/exhaustive-deps [] ); diff --git a/client/components/shared/Form/ImageUploader.tsx b/client/components/shared/Form/ImageUploader.tsx index db0807fd..2a3670ae 100644 --- a/client/components/shared/Form/ImageUploader.tsx +++ b/client/components/shared/Form/ImageUploader.tsx @@ -1,5 +1,7 @@ import React, { useCallback, type ChangeEvent } from "react"; -import ImageUploader from "@components/shared/ImageUploader"; +import useImageUpload from "@hooks/useImageUpload"; +import styles from "./styles.module.scss"; +import Image from "next/image"; interface Props { value?: string | null; @@ -13,33 +15,30 @@ const PostFormImageUploader = ({ value, onChange }: Props) => { }, [onChange] ); - const onImageUploadeSuccess = useCallback( - (imageUrl: string) => { - onChange(imageUrl); + + const onImageUploadSuccess = useCallback( + ({ url }: { url: string }) => { + onChange(url); }, [onChange] ); + const { handleClickUploadButton } = useImageUpload({ onImageUploadSuccess }); + return ( - +
- - +
- - + {value && 썸네일} +
); }; diff --git a/client/components/shared/Form/styles.module.scss b/client/components/shared/Form/styles.module.scss index d03c9246..1a9d53d8 100644 --- a/client/components/shared/Form/styles.module.scss +++ b/client/components/shared/Form/styles.module.scss @@ -1,3 +1,5 @@ +@import "utils/utils.scss"; + .DivWithLabel { display: flex; flex-direction: column; @@ -65,3 +67,23 @@ monospace; } } + +.ImageUploader { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 10px; + div { + display: flex; + align-items: center; + gap: 20px; + } + button { + @include StyledButton; + height: 43px; + white-space: nowrap; + } + input { + @include StyledInput; + } +} diff --git a/client/components/shared/ImageUploader/ImageUploader.tsx b/client/components/shared/ImageUploader/ImageUploader.tsx deleted file mode 100644 index 0583f53f..00000000 --- a/client/components/shared/ImageUploader/ImageUploader.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { - type InputHTMLAttributes, - type ComponentProps, - type PropsWithChildren, -} from "react"; -import styles from "./styles.module.scss"; -import { isNull } from "@utils"; -import Image from "next/image"; -import { ServerURL } from "@constants"; -interface UploadButtonProps { - onUploadeSuccess: (imageUrl: string) => void; - text: string; -} - -const UploadButton = ({ onUploadeSuccess, text }: UploadButtonProps) => { - const handleClick = () => { - const inputElem = document.createElement("input"); - INPUT_ATTRIBUTE.forEach(({ attr, value }) => { - inputElem.setAttribute(attr, value); - }); - document.body.appendChild(inputElem); - inputElem.click(); - inputElem.onchange = async () => { - const file = inputElem.files; - - if (!file) { - return; - } - - const formData = new FormData(); - formData.append("img", file[0]); - - const response = await fetch(`${ServerURL}/uploads`, { - method: "post", - body: formData, - }); - - const data: { url: string; uploaded: boolean } = await response.json(); - - onUploadeSuccess(data.url); - document.body.removeChild(inputElem); - }; - }; - - return ( - - ); -}; - -const ImageUrlInput = (props: InputHTMLAttributes) => { - return ; -}; - -const UploadedImage = (props: ComponentProps) => { - if (isNull(props.src as string)) { - return null; - } - - return uploadedImage; -}; - -const ImageUploader = Object.assign( - ({ children }: PropsWithChildren) => ( -
{children}
- ), - { - UploadButton, - Image: UploadedImage, - ImageUrlInput, - } -); - -export default ImageUploader; - -const INPUT_ATTRIBUTE = [ - { attr: "type", value: "file" }, - { attr: "accept", value: "image/*" }, -]; diff --git a/client/components/shared/ImageUploader/index.ts b/client/components/shared/ImageUploader/index.ts deleted file mode 100644 index 6b4b1c8e..00000000 --- a/client/components/shared/ImageUploader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ImageUploader"; diff --git a/client/components/shared/ImageUploader/styles.module.scss b/client/components/shared/ImageUploader/styles.module.scss deleted file mode 100644 index 3f6d395a..00000000 --- a/client/components/shared/ImageUploader/styles.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import "utils/utils.scss"; - -.ImageUploader { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 10px; - div { - display: flex; - align-items: center; - gap: 20px; - } - button { - @include StyledButton; - height: 43px; - white-space: nowrap; - } - input { - @include StyledInput; - } -} diff --git a/server/build/database/models/Posts.js b/server/build/database/models/Posts.js index 7776c20c..825c9e84 100644 --- a/server/build/database/models/Posts.js +++ b/server/build/database/models/Posts.js @@ -30,7 +30,7 @@ const initPosts = () => { allowNull: false, //필수 }, thumbNailUrl: { - type: sequelize_1.DataTypes.STRING(100), + type: sequelize_1.DataTypes.TEXT, allowNull: true, //필수 }, views: { diff --git a/server/src/database/models/Posts.ts b/server/src/database/models/Posts.ts index c905acc0..67a56a65 100644 --- a/server/src/database/models/Posts.ts +++ b/server/src/database/models/Posts.ts @@ -48,7 +48,7 @@ export const initPosts = () => { allowNull: false, //필수 }, thumbNailUrl: { - type: DataTypes.STRING(100), + type: DataTypes.TEXT, allowNull: true, //필수 }, views: { From c93cbee55737af009d2e1ae75311c71e405d83ea Mon Sep 17 00:00:00 2001 From: BY-juun Date: Wed, 29 Nov 2023 21:28:55 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20giscus=20script=20attribute=20?= =?UTF-8?q?=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/components/post/PostComments.tsx | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/client/components/post/PostComments.tsx b/client/components/post/PostComments.tsx index 4d1a7055..bf4452f7 100644 --- a/client/components/post/PostComments.tsx +++ b/client/components/post/PostComments.tsx @@ -25,20 +25,12 @@ const Comments = () => { const scriptElem = document.createElement("script"); scriptElem.src = `${src}/client.js`; - scriptElem.setAttribute("data-repo", "BY-juun/Blog"); - scriptElem.setAttribute("data-repo-id", "MDEwOlJlcG9zaXRvcnkzODc4MTYxNjA="); - scriptElem.setAttribute("data-category", "Comments"); - scriptElem.setAttribute("data-category-id", "DIC_kwDOFx2a4M4CYB9c"); - scriptElem.setAttribute("data-mapping", "pathname"); - scriptElem.setAttribute("data-reactions-enabled", "1"); - scriptElem.setAttribute("data-emit-metadata", "0"); - scriptElem.setAttribute("data-input-position", "bottom"); + + Object.entries(SCRIPT_ATTRIBUTES).forEach(([key, value]) => { + scriptElem.setAttribute(key, value); + }); scriptElem.setAttribute("data-theme", theme); - scriptElem.setAttribute("data-lang", "ko"); - scriptElem.setAttribute("data-loading", "lazy"); - scriptElem.setAttribute("crossorigin", "anonymous"); scriptElem.async = true; - commentWrapperRef.current?.appendChild(scriptElem); return () => { @@ -56,4 +48,18 @@ const Comments = () => { return
; }; +const SCRIPT_ATTRIBUTES = { + "data-repo": "BY-juun/Blog", + "data-repo-id": "MDEwOlJlcG9zaXRvcnkzODc4MTYxNjA=", + "data-category": "Comments", + "data-category-id": "DIC_kwDOFx2a4M4CYB9c", + "data-mapping": "pathname", + "data-reactions-enabled": "1", + "data-emit-metadata": "0", + "data-input-position": "bottom", + "data-lang": "ko", + "data-loading": "lazy", + crossorigin: "anonymous", +}; + export default Comments;