좋아! 리액트 프론트에서 방금 만든 백엔드 API/웹소켓에 실전 연결하는 방법을 한 번에 정리해줄게. 그대로 복붙해서 시작하면 돼.


0) 환경변수 설정(.env)

# REST API base
REACT_APP_API_BASE=https://api.dongcheolcoding.life/api
# WS/STOMP endpoint (네 서버 주소에 맞춰 수정)
REACT_APP_WS_URL=wss://api.dongcheolcoding.life/ws-chat


1) HTTP 클라이언트(axios 인스턴스 + JWT 헤더)

// src/lib/http.ts
import axios from "axios";

export const http = axios.create({
  baseURL: process.env.REACT_APP_API_BASE, // /api 로 끝남
});

http.interceptors.request.use((config) => {
  const token = localStorage.getItem("access_token"); // 로그인 시 저장했다고 가정
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});


2) API 래퍼 (강의 CRUD/조회/진도)

// src/api/lectures.ts
import { http } from "../lib/http";

export type LectureCreateRequest = {
  title: string; description?: string; orderIndex: number; isPublic: boolean;
  durationSec?: number; // with-video 에서만 선택
};

export const LectureApi = {
  // ADMIN: 강의 생성(영상 없이 JSON)
  create: (courseId: number, body: LectureCreateRequest) =>
    http.post(`/courses/${courseId}/lectures`, body),

  // ADMIN: 강의+영상 한 번에(멀티파트)
  createWithVideo: (courseId: number, body: LectureCreateRequest, file?: File) => {
    const fd = new FormData();
    fd.append("data", new Blob([JSON.stringify(body)], { type: "application/json" }));
    if (file) fd.append("video", file);
    return http.post(`/courses/${courseId}/lectures:with-video`, fd, {
      headers: { "Content-Type": "multipart/form-data" },
    });
  },

  // ADMIN
  update: (lectureId: number, body: Partial<LectureCreateRequest>) =>
    http.patch(`/lectures/${lectureId}`, body),

  // ADMIN
  remove: (lectureId: number) => http.delete(`/lectures/${lectureId}`),

  // MEMBER/ADMIN: 코스 내 강의 목록
  listByCourse: (courseId: number) =>
    http.get(`/courses/${courseId}/lectures`), // => LectureSummaryDto[]

  // MEMBER/ADMIN: 강의 상세(READY면 videoUrl 포함)
  getDetail: (lectureId: number) => http.get(`/lectures/${lectureId}`),

  // MEMBER: 진도 업데이트
  updateProgress: (lectureId: number, watchedSec: number) =>
    http.post(`/lectures/${lectureId}/progress`, { watchedSec }),
};


3) React 컴포넌트 예시

3.1 코스 강의 목록

// src/pages/CourseLectures.tsx
import { useEffect, useState } from "react";
import { LectureApi } from "../api/lectures";

export default function CourseLectures({ courseId }: { courseId: number }) {
  const [lectures, setLectures] = useState<any[]>([]);
  useEffect(() => {
    LectureApi.listByCourse(courseId).then(res => setLectures(res.data.data));
  }, [courseId]);

  return (
    <div>
      <h2>강의 목록</h2>
      <ul>
        {lectures.map((l) => (
          <li key={l.id}>
            #{l.orderIndex} {l.title} — {Math.round(l.progress * 100)}%
          </li>
        ))}
      </ul>
    </div>
  );
}

3.2 강의 보기 + 진도 업데이트(주기적 전송)

// src/pages/LecturePlayer.tsx
import { useEffect, useRef, useState } from "react";
import { LectureApi } from "../api/lectures";

export default function LecturePlayer({ lectureId }: { lectureId: number }) {
  const [detail, setDetail] = useState<any>(null);
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    LectureApi.getDetail(lectureId).then(res => setDetail(res.data.data));
  }, [lectureId]);

  // 5초마다 watchedSec 전송 (누적기준)
  useEffect(() => {
    const iv = setInterval(() => {
      const cur = Math.floor(videoRef.current?.currentTime || 0);
      LectureApi.updateProgress(lectureId, cur).catch(() => {});
    }, 5000);
    return () => clearInterval(iv);
  }, [lectureId]);

  if (!detail) return <div>Loading...</div>;
  return (
    <div>
      <h2>{detail.title}</h2>
      {detail.videoUrl ? (
        <video ref={videoRef} src={detail.videoUrl} controls style={{ width: 800 }} />
      ) : (
        <div>영상 준비 중…</div>
      )}
      <div>현재 강의 진도: {Math.round(detail.progress * 100)}%</div>
    </div>
  );
}

3.3 강의 생성 + 영상 업로드(ADMIN)

// src/pages/AdminCreateLecture.tsx
import { useState } from "react";
import { LectureApi } from "../api/lectures";

export default function AdminCreateLecture({ courseId }: { courseId: number }) {
  const [title, setTitle] = useState("");
  const [orderIndex, setOrderIndex] = useState(1);
  const [isPublic, setIsPublic] = useState(true);
  const [file, setFile] = useState<File | undefined>();

  const submit = async () => {
    await LectureApi.createWithVideo(courseId, { title, orderIndex, isPublic }, file);
    alert("생성 완료");
  };

  return (
    <div>
      <h2>강의 생성</h2>
      <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="제목" />
      <input type="number" value={orderIndex} onChange={(e) => setOrderIndex(+e.target.value)} />
      <label><input type="checkbox" checked={isPublic} onChange={e=>setIsPublic(e.target.checked)} />공개</label>
      <input type="file" accept="video/*" onChange={e=>setFile(e.target.files?.[0])} />
      <button onClick={submit}>생성</button>
    </div>
  );
}


4) 실시간 진도 반영(STOMP / SockJS)