좋아! 리액트 프론트에서 방금 만든 백엔드 API/웹소켓에 실전 연결하는 방법을 한 번에 정리해줄게. 그대로 복붙해서 시작하면 돼.
# 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
// 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;
});
// 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 }),
};
// 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>
);
}
// 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>
);
}
// 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>
);
}