๐Ÿ–ผ๏ธ ๊ฒŒ์‹œ๊ธ€ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ( + drag & drop )

๋ฐ•์ƒ์€ยท2022๋…„ 4์›” 28์ผ
1

โœ๏ธ blelog โœ๏ธ

๋ชฉ๋ก ๋ณด๊ธฐ
3/13

๐Ÿ™Œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํ๋ฆ„

1. ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ ์ด์šฉ

  1. <input type="file" />๋Œ€์‹ ํ•˜๋Š” ๋ฒ„ํŠผ ํด๋ฆญ
  2. ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ๋กœ ์ด๋ฏธ์ง€ ํ•˜๋‚˜ ์„ ํƒ
  3. martipark/form-dataํ˜•์‹์œผ๋กœ ๋ฐฑ์—”๋“œ๋กœ ์ด๋ฏธ์ง€ ์ „์†ก
  4. ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ ์Šคํ”ผ๋„ˆ ๋žœ๋”๋ง
  5. ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ url์„ ๋ฐ›์Œ ( photoUrl )
  6. ํ•ด๋‹น url์„ ์ด์šฉํ•ด์„œ ![์ด๋ฏธ์ง€](photoUrl)ํ˜•ํƒœ๋กœ contents์— ๋„ฃ์Œ

2. drag & drop ์ด์šฉ

  1. ์ด๋ฏธ์ง€ ๋“œ๋ž˜๊ทธ ์‹œ ๋“œ๋ž˜๊ทธ ์˜์—ญ์— ๋“œ๋ž˜๊ทธ ํ™”๋ฉด ๋žœ๋”๋ง
  2. ์˜์—ญ์— ์ด๋ฏธ์ง€ ๋“œ๋ž˜๊ทธ ์‹œ martipark/form-dataํ˜•์‹์œผ๋กœ ๋ฐฑ์—”๋“œ๋กœ ์ด๋ฏธ์ง€ ์ „์†ก
  3. ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ ์Šคํ”ผ๋„ˆ ๋žœ๋”๋ง
  4. ๋ฐฑ์—”๋“œ์—์„œ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ url์„ ๋ฐ›์Œ ( photoUrl )
  5. ํ•ด๋‹น url์„ ์ด์šฉํ•ด์„œ ![์ด๋ฏธ์ง€](photoUrl)ํ˜•ํƒœ๋กœ contents์— ๋„ฃ์Œ

๐Ÿ‘‹ drag & drop ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์ฝ”๋“œ ์˜ˆ์‹œ

// 2022/04/28 - ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋กœ๋”ฉ ๋ณ€์ˆ˜ - by 1-blue
const [uploadLoading, setUploadLoading] = useState(false); 
// drag & drop์œผ๋กœ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌํ•˜๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
const onUploadPhotoByDrop = useCallback(
  async (e: any) => {
    e.preventDefault();

    setUploadLoading(true);

    try {
      const formData = new FormData();
      formData.append("photo", e.dataTransfer.files[0]);
      const { photoUrl }: PhotoResponse = await fetch("/api/photo", {
        method: "POST",
        body: formData,
      }).then((res) => res.json());
      setValue(
        "contents",
        getValues("contents") + `\n![์ด๋ฏธ์ง€](${photoUrl})`
      );
      toast.success("์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.");
    } catch (error) {
      toast.error("์ด๋ฏธ์ง€ ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
    }

    setUploadLoading(false);
    setIsDragging(false);
  },
  [getValues, setValue, setUploadLoading, setIsDragging]
);

// jsx ์˜์—ญ
return (
  <article
    onDragOver={() => setIsDragging(true)}
    onDragLeave={() => setIsDragging(false)}
  >
    <section className="flex-1 dark:bg-zinc-800 bg-zinc-200 p-4">
      {isDragging ? (
        <div
          className="flex flex-col h-full justify-center items-center"
          onDragOver={(e) => e.preventDefault()}
          onDrop={onUploadPhotoByDrop}
        >
          <span>๐Ÿ–ผ๏ธ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๊ธฐ์— ๋“œ๋ž˜๊ทธ ํ•ด์ฃผ์„ธ์š”!</span>
          <Icon icon={ICON.PHOTO} className="w-40 h-40" />
        </div>
      ) : (
        <>
          // markdown ํ˜•์‹์œผ๋กœ ์ž…๋ ฅ๋ฐ›๋Š” form... ์ƒ๋žต
        </>
      )}
     </section>
   </article>
)

๐Ÿ‘ <input type="file" />์„ ์ด์šฉํ•œ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ

// 2022/04/28 - ์ด๋ฏธ์ง€ input ref - by 1-blue
const photoRef = useRef<HTMLInputElement>(null);
// 2022/04/28 - ์ด๋ฏธ์ง€ ๋“œ๋ž˜๊ทธ์ค‘์ธ์ง€ ํŒ๋‹จํ•  ๋ณ€์ˆ˜ - by 1-blue
const [isDragging, setIsDragging] = useState(false);
// 2022/04/28 - ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋กœ๋”ฉ ๋ณ€์ˆ˜ - by 1-blue
const [uploadLoading, setUploadLoading] = useState(false);
// 2022/04/28 - ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ( ํŒŒ์ผ ํƒ์ƒ‰๊ธฐ ์ด์šฉ ) - by 1-blue
const onUploadPhotoByExplorer = useCallback(
  async (e: any) => {
    setUploadLoading(true);

    try {
      const formData = new FormData();
      formData.append("photo", e.target.files[0]);
      const { photoUrl }: PhotoResponse = await fetch("/api/photo", {
        method: "POST",
        body: formData,
      }).then((res) => res.json());
      setValue(
        "contents",
        getValues("contents") + `\n![์ด๋ฏธ์ง€](${photoUrl})`
      );
      toast.success("์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.");
    } catch (error) {
      toast.error("์ด๋ฏธ์ง€ ์—…๋กœ๋“œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
    }

    setUploadLoading(false);
    setIsDragging(false);
  },
  [setUploadLoading, getValues, setValue, setIsDragging]
);

// jsx ์˜์—ญ
return (
  <article
    onDragOver={() => setIsDragging(true)}
    onDragLeave={() => setIsDragging(false)}
  >
    <section className="flex-1 dark:bg-zinc-800 bg-zinc-200 p-4">
      {isDragging ? (
        <div
          className="flex flex-col h-full justify-center items-center"
          onDragOver={(e) => e.preventDefault()}
          onDrop={onUploadPhotoByDrop}
        >
          <span>๐Ÿ–ผ๏ธ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๊ธฐ์— ๋“œ๋ž˜๊ทธ ํ•ด์ฃผ์„ธ์š”!</span>
          <Icon icon={ICON.PHOTO} className="w-40 h-40" />
        </div>
      ) : (
        <form
          className="flex flex-col h-full"
          onSubmit={handleSubmit(onCreatePost)}
        >
          <input
            type="file"
            accept="image/*"
            ref={photoRef}
            onChange={onUploadPhotoByExplorer}
            hidden
          />
          <button
            type="button"
            onClick={() => photoRef.current?.click()}
            className="p-1 rounded-md hover:text-white hover:bg-black dark:hover:text-black dark:hover:bg-white focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2"
          >
            <Icon icon={ICON.PHOTO} />
          </button>
          
          // ๋‹ค๋ฅธ ํƒœ๊ทธ๋“ค์€ ์ƒ๋žต...
        </form>
      )}
     </section>
   </article>
)

๐Ÿค” ์ฒ˜์Œ ์ƒ๊ฐํ–ˆ๋˜ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ฐฉ์‹

  1. ์ด๋ฏธ์ง€๋ฅผ ํƒ์ƒ‰๊ธฐ or drag & drop API๋กœ ๋ฐ›์Œ
  2. ๋ฐ›์€ File์„ ๋ฐฐ์—ด์— ๋„ฃ์Œ
  3. FileReader๋ฅผ ์ด์šฉํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ base64ํ˜•ํƒœ๋กœ ์ฒ˜๋ฆฌํ•จ
  4. base64๋ฅผ ํ™”๋ฉด์— ๋žœ๋”๋งํ•จ
  5. ์ดํ›„ ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ ์‹œ File๋ฐฐ์—ด์„ ๋ฐฑ์—”๋“œ๋กœ ๋„˜๊ฒจ์ค˜์„œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•จ
  • ๋ฐœ์ƒํ•œ ๋ฌธ์ œ
    1. ์ด๋ฏธ์ง€๊ฐ€ ์กฐ๊ธˆ๋งŒ ํฌ๋”๋ผ๋„ base64์˜ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ปค์ ธ์„œ react-markdown์ด ๊ณ ์žฅ ๋‚˜์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฉˆ์ถฐ๋ฒ„๋ฆผ
    2. ๊ฒŒ์‹œ๊ธ€์„ ์ž„์‹œ์ €์žฅ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” base64 ์ž์ฒด๋ฅผ DB์— ๋„ฃ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒ ( ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„ )
    ๋งŒ์•ฝ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ฒŒ์‹œ๊ธ€์„ ์ž„์‹œ ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ด๋ฏธ ์ €์žฅํ•œ ๊ฑด์ง€ ๊ฒ€์‚ฌํ•˜๋Š” ๋กœ์ง์ด ์ถ”๊ฐ€๋กœ ํ•„์š”ํ•จ

๐Ÿ˜ฅ ๋ฌธ์ œ

์ด๋ฏธ์ง€๋ฅผ ๋“œ๋ž˜๊ทธํ•ด์„œ ํŽ˜์ด์ง€ ๋‚ด๋ถ€๋กœ ๊ฐ€์ ธ์˜จ ๊ฒฝ์šฐ์— ์ด๋ฏธ์ง€ ๋“œ๋ž˜๊ทธ ์˜์—ญ์„ ๋žœ๋”๋งํ•˜๋Š”๋ฐ ํ•ด๋‹น ์˜์—ญ์ด ๊นœ๋นก๊ฑฐ๋ฆฌ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
๋“œ๋ž˜๊ทธ ์˜์—ญ์—์„œ ๋‚ด๋ถ€ ํƒœ๊ทธ์˜ ์œ„๋ฅผ ์ง€๋‚˜๊ฐˆ ๊ฒฝ์šฐ์—๋งŒ isDragging๊ฐ’์ด false๊ฐ€ ๋ผ์„œ ์ฆ‰, onDragLeave ์ด๋ฒคํŠธ๊ฐ€ ์ž‘๋™ํ•˜๋Š”๋ฐ ์ž‘๋™ํ•˜๋Š” ์ด์œ ๋ฅผ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ•ด์„œ ํŠธ๋ ๋กœ์— ๊ธฐ๋กํ•ด๋‘๊ณ  ๋‹ค์Œ์— ์ฒ˜๋ฆฌํ•  ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค.

๐Ÿ‘‡ ์ฐธ๊ณ 

0๊ฐœ์˜ ๋Œ“๊ธ€