프로젝트 기간: 2024년 2월 - 2024년 8월 (약 6개월)
🎯 서비스 핵심 기능
1. 통합 학습 관리 시스템 (LMS)
2. 세분화된 멤버십 시스템
3. 스트리밍 영상 플레이어
4. 보안 및 접근 제어
5. 관리자 시스템 (Admin)
🔧 내가 진행한 부분들
Admin 사이트 (kma-lms-admin)
퍼블리싱 작업
기능 개발
코드 컨벤션 & 구조 개선
@repo/ui, @repo/utils 등 모노레포 패키지 연동LMS Web 사이트 (kma-lms-web)
영상 플레이어 시스템 수정 및 고도화
// 10초 간격 학습 로그 저장 로직
const onTimeUpdate = async () => {
const sec = Math.floor(video.currentTime);
if (sec - lastLoggedTimeRef.current >= 10) {
lastLoggedTimeRef.current = sec;
await saveVideoLog({ eduSeq: cttSeq, watchTm: sec });
await updateTime({ prvtEnrolCurrPartSeq, enrolTm: sec, enrolYn: isCompleted ? 'Y' : 'N' });
}
// 90% 이상 시청 시 완료 처리
if (!isCompleted && duration > 0 && current / duration >= 0.9) {
setIsCompleted(true);
onPartComplete?.(prvtEnrolCurrPartSeq);
}
};
기기 제한 및 세션 관리
// 기기 등록 검증 및 중복 로그인 방지 const handleCheckDevice = async () => { const deviceArr = deviceArrRef.current; if (deviceArr.length >= 3) { customAlert('등록된 기기가 최대 3대를 초과하여 콘텐츠를 감상할 수 없습니다.'); return false; } return true; };
멤버십 분기 처리
💪 어려웠던 점과 해결 방법
1. 백엔드 영상 스트림 연동
문제 상황
해결 방법
// HLS.js를 활용한 스트리밍 처리
useEffect(() => {
if (Hls.isSupported()) {
const hls = new Hls({
xhrSetup: (xhr) => {
xhr.setRequestHeader('Authorization', `Bearer ${session?.accessToken}`);
},
});
hls.loadSource(src);
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 네이티브 HLS 지원
video.src = src;
}
}, [src, session?.accessToken]);
xhrSetup 옵션으로 스트리밍 요청에 인증 헤더 자동 주입2. IP/디바이스별 진행률 클라이언트 관리
문제 상황
해결 방법
// 탭 전환 시에도 진도 보존 const onVisibility = () => { if (document.visibilityState === 'hidden') { lastTimeRef.current = video.currentTime; if (!video.paused) { video.pause(); isAutoPausedRef.current = true; } } else if (document.visibilityState === 'visible') { if (isAutoPausedRef.current) { video.play().catch(console.warn); isAutoPausedRef.current = false; } } };
visibilitychange 이벤트 활용한 탭 전환 감지3. 세분화된 멤버십별 분기 처리
문제 상황
해결 방법
// Zustand 스토어에서 멤버십 상태 통합 관리 const isMemberShip = kmaMemDiv === '01' || studMemGrd === '11' || studMemGrd === '12' || studMemGrd === '13'; // 등급별 상수 매핑 const KMA_MEM_GRD: Record<string, string> = { '01': 'KMA 정회원', '15': 'KMA 이사단', '16': 'KMA 정회원', }; const STUD_MEM_GRD: Record<string, string> = { '11': 'LITE', '12': 'CORE', '13': 'PREMIUM', '14': 'ALL-IN-ONE', };
isMemberShip 플래그와 등급 코드만으로 분기 처리