강의 메타데이터 + 영상 파일을 선택하고, 영상 길이(durationSec)을 자동으로 읽어서 DTO에 포함해 **/api/courses/{courseId}/lectures/with-video**로 업로드합니다.


📌 LectureUploader.tsx

import React, { useState } from "react";

/** 영상 길이 읽기 */
function readVideoDuration(file: File): Promise<number> {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file);
    const v = document.createElement("video");
    v.preload = "metadata";

    v.onloadedmetadata = () => {
      if (!isFinite(v.duration) || v.duration <= 0) {
        reject(new Error("영상 길이를 읽을 수 없습니다."));
        URL.revokeObjectURL(url);
        return;
      }
      const seconds = Math.round(v.duration);
      URL.revokeObjectURL(url);
      resolve(seconds);
    };

    v.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error("영상 메타데이터 로딩 실패"));
    };

    v.src = url;
  });
}

export default function LectureUploader() {
  const [title, setTitle] = useState("");
  const [desc, setDesc] = useState("");
  const [orderIndex, setOrderIndex] = useState(0);
  const [isPublic, setIsPublic] = useState(true);
  const [file, setFile] = useState<File | null>(null);
  const [loading, setLoading] = useState(false);

  /** 업로드 실행 */
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!file) {
      alert("영상을 선택하세요.");
      return;
    }

    try {
      setLoading(true);

      // 1) 영상 길이 읽기
      const durationSec = await readVideoDuration(file);
      const sizeBytes = file.size;

      // 2) DTO 생성
      const dto = {
        title,
        description: desc,
        orderIndex,
        isPublic,
        durationSec,
        sizeBytes,
      };

      // 3) FormData 구성
      const form = new FormData();
      form.append("data", JSON.stringify(dto));
      form.append("video", file);

      // 4) API 호출
      const courseId = 1; // 👉 테스트할 courseId 값 넣기
      const res = await fetch(
        `https://api.dongcheolcoding.life/api/courses/${courseId}/lectures/with-video`,
        {
          method: "POST",
          body: form,
          credentials: "include", // JWT 쿠키 인증이라면 필요
        }
      );

      if (!res.ok) {
        const text = await res.text();
        throw new Error(`업로드 실패 (${res.status}): ${text}`);
      }

      const json = await res.json();
      console.log("✅ 업로드 성공:", json);
      alert("강의가 등록되었습니다.");
    } catch (err: any) {
      console.error(err);
      alert(err.message ?? "업로드 실패");
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 500, margin: "2rem auto" }}>
      <h2>강의 업로드</h2>

      <div>
        <label>제목</label>
        <inputtype="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          required
        />
      </div>

      <div>
        <label>설명</label>
        <textareavalue={desc}
          onChange={(e) => setDesc(e.target.value)}
          rows={3}
        />
      </div>

      <div>
        <label>순서</label>
        <inputtype="number"
          value={orderIndex}
          onChange={(e) => setOrderIndex(Number(e.target.value))}
        />
      </div>

      <div>
        <label>
          공개
          <inputtype="checkbox"
            checked={isPublic}
            onChange={(e) => setIsPublic(e.target.checked)}
          />
        </label>
      </div>

      <div>
        <label>영상 파일</label>
        <inputtype="file"
          accept="video/*"
          onChange={(e) => setFile(e.target.files?.[0] ?? null)}
        />
      </div>

      <button type="submit" disabled={loading}>
        {loading ? "업로드 중..." : "업로드"}
      </button>
    </form>
  );
}


📌 요약

  1. 사용자가 영상 선택 → readVideoDuration으로 길이 읽음.
  2. DTO에 durationSec, sizeBytes 포함해서 JSON 직렬화 → FormData의 data에 넣음.
  3. video 파트에 실제 파일 추가.
  4. 백엔드 /lectures/with-video API로 전송.
  5. 성공 시 응답(LectureDetailDto) 확인 가능.