-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [NDD-103] Member API E2E 테스트 (1h / 1h) (#29) * test: Member API E2E 테스트 코드 작성 1. /api/member GET API 에 대한 성공 테스트 2. /api/member GET API에서 유효하지 않은 토큰 사용으로 인한 실패 테스트 위의 2가지의 경우에 대한 테스트 코드 작성 * style: lint 적용 * test: 인수 테스트 코드 로직 변경 완료 GET /api/member에 대한 E2E 테스트 완료 * style: Window 개행문자 관련 에러 해결을 위한 lint 설정 추가 * feat: TestingModule을 생성하는 Util 메서드 구현 * [NDD-87] Interview 페이지 camera 컴포넌트 기능 부여 (7h/8h) (#30) * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: InterviewCamera 내부 state 정의 * feat: 현재 브라우저에서 지원가능 한 MimeType을 명시 * feat: 현재 Media에 연결 기능 구현 * feat: 마운트시 바로 stream 연결 * feat: Record 시작함수 구현 * feat: record stop 함수 구현 * feat: Record Download 함수 구현 * feat: video UI 기능 구현 * fix: log 문제 * fix: InterviewCamera 파일시스템 문제 해결 * chore: 상대경로 수정 * chore: CameraRef 이름 변경 --------- Co-authored-by: quiet-honey <[email protected]>
- Loading branch information
1 parent
7c05a3b
commit 40c775f
Showing
6 changed files
with
213 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import {CorsOptions} from "@nestjs/common/interfaces/external/cors-options.interface"; | ||
import {CORS_HEADERS, CORS_ORIGIN} from "./cors.secure"; | ||
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; | ||
import { CORS_HEADERS, CORS_ORIGIN } from './cors.secure'; | ||
|
||
export const CORS_CONFIG: CorsOptions = { | ||
origin: CORS_ORIGIN, | ||
credentials: true, | ||
exposedHeaders: CORS_HEADERS, | ||
} | ||
origin: CORS_ORIGIN, | ||
credentials: true, | ||
exposedHeaders: CORS_HEADERS, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,15 @@ | ||
import { Test } from '@nestjs/testing'; | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { MemberController } from './member.controller'; | ||
import { MemberResponse } from '../dto/memberResponse'; | ||
import { Request } from 'express'; | ||
import { Member } from '../entity/member'; | ||
import { ManipulatedTokenNotFiltered } from 'src/token/exception/token.exception'; | ||
import { INestApplication } from '@nestjs/common'; | ||
import { AppModule } from 'src/app.module'; | ||
import * as request from 'supertest'; | ||
import { AuthModule } from 'src/auth/auth.module'; | ||
import { AuthService } from 'src/auth/service/auth.service'; | ||
import { OAuthRequest } from 'src/auth/interface/auth.interface'; | ||
|
||
describe('MemberController', () => { | ||
let memberController: MemberController; | ||
|
@@ -47,3 +53,55 @@ describe('MemberController', () => { | |
).toThrow(ManipulatedTokenNotFiltered); | ||
}); | ||
}); | ||
|
||
describe('MemberController (E2E Test)', () => { | ||
let app: INestApplication; | ||
let authService: AuthService; | ||
|
||
beforeAll(async () => { | ||
const moduleFixture: TestingModule = await Test.createTestingModule({ | ||
imports: [AppModule, AuthModule], | ||
}).compile(); | ||
|
||
app = moduleFixture.createNestApplication(); | ||
await app.init(); | ||
|
||
authService = moduleFixture.get<AuthService>(AuthService); | ||
}); | ||
|
||
it('GET /api/member (회원 정보 반환 성공)', async () => { | ||
const oauthRequestFixture = { | ||
email: '[email protected]', | ||
name: 'fixture', | ||
img: 'https://test.com', | ||
} as OAuthRequest; | ||
|
||
const validToken = (await authService.login(oauthRequestFixture)).replace( | ||
'Bearer ', | ||
'', | ||
); | ||
const response = await request(app.getHttpServer()) | ||
.get('/api/member') | ||
.set('Authorization', `Bearer ${validToken}`) | ||
.expect(200); | ||
|
||
console.log(response.body); | ||
|
||
expect(response.body.email).toBe(oauthRequestFixture.email); | ||
expect(response.body.nickname).toBe(oauthRequestFixture.name); | ||
expect(response.body.profileImg).toBe(oauthRequestFixture.img); | ||
}); | ||
|
||
it('GET /api/member (유효하지 않은 토큰 사용으로 인한 회원 정보 반환 실패)', async () => { | ||
const invalidToken = 'INVALID_TOKEN'; | ||
|
||
await request(app.getHttpServer()) | ||
.get('/api/member') | ||
.set('Authorization', `Bearer ${invalidToken}`) | ||
.expect(401); | ||
}); | ||
|
||
afterAll(async () => { | ||
await app.close(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { ModuleMetadata } from '@nestjs/common'; | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
|
||
export const createTestModuleFixture = async ( | ||
imports: unknown, | ||
controllers: unknown, | ||
providers: unknown, | ||
) => { | ||
const moduleFixture: TestingModule = await Test.createTestingModule({ | ||
imports: imports, | ||
controllers: controllers, | ||
providers: providers, | ||
} as ModuleMetadata).compile(); | ||
|
||
return moduleFixture; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,143 @@ | ||
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; | ||
import { css } from '@emotion/react'; | ||
|
||
const InterviewCamera: React.FC = () => { | ||
const [stream, setStream] = useState<MediaStream | null>(null); | ||
const [recording, setRecording] = useState(false); | ||
const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]); | ||
const [selectedMimeType, setSelectedMimeType] = useState(''); | ||
|
||
const mirrorVideoRef = useRef<HTMLVideoElement>(null); | ||
const mediaRecorderRef = useRef<MediaRecorder | null>(null); | ||
|
||
useLayoutEffect(() => { | ||
const mimeTypes = getSupportedMimeTypes(); | ||
if (mimeTypes.length > 0) { | ||
setSelectedMimeType(mimeTypes[0]); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (!stream) { | ||
void getMedia(); | ||
} | ||
|
||
return () => { | ||
if (stream) { | ||
stream.getTracks().forEach((track) => track.stop()); | ||
} | ||
}; | ||
}, [stream]); | ||
|
||
const getMedia = async () => { | ||
try { | ||
const constraints = { | ||
audio: { | ||
echoCancellation: { exact: true }, | ||
}, | ||
video: { | ||
width: 1280, | ||
height: 720, | ||
}, | ||
}; | ||
const mediaStream = | ||
await navigator.mediaDevices.getUserMedia(constraints); | ||
setStream(mediaStream); | ||
if (mirrorVideoRef.current) { | ||
mirrorVideoRef.current.srcObject = mediaStream; | ||
} | ||
} catch (e) { | ||
console.log(`현재 마이크와 카메라가 연결되지 않았습니다`); | ||
} | ||
}; | ||
|
||
const handleStartRecording = () => { | ||
setRecordedBlobs([]); | ||
try { | ||
mediaRecorderRef.current = new MediaRecorder(stream as MediaStream, { | ||
mimeType: selectedMimeType, | ||
}); | ||
mediaRecorderRef.current.ondataavailable = (event) => { | ||
if (event.data && event.data.size > 0) { | ||
setRecordedBlobs((prev) => [...prev, event.data]); | ||
} | ||
}; | ||
mediaRecorderRef.current.start(); | ||
setRecording(true); | ||
} catch (e) { | ||
console.log(`MediaRecorder error`); | ||
} | ||
}; | ||
|
||
const handleStopRecording = () => { | ||
if (mediaRecorderRef.current) { | ||
mediaRecorderRef.current.stop(); | ||
} | ||
setRecording(false); | ||
}; | ||
|
||
const handleDownload = () => { | ||
const blob = new Blob(recordedBlobs, { type: selectedMimeType }); | ||
const url = window.URL.createObjectURL(blob); | ||
const a = document.createElement('a'); | ||
a.style.display = 'none'; | ||
a.href = url; | ||
a.download = 'recorded.webm'; | ||
document.body.appendChild(a); | ||
a.click(); | ||
setTimeout(() => { | ||
document.body.removeChild(a); | ||
window.URL.revokeObjectURL(url); | ||
}, 100); | ||
}; | ||
|
||
const getSupportedMimeTypes = () => { | ||
const types = [ | ||
'video/webm; codecs=vp8', | ||
'video/webm; codecs=vp9', | ||
'video/webm; codecs=h264', | ||
'video/mp4; codecs=h264', | ||
]; | ||
return types.filter((type) => MediaRecorder.isTypeSupported(type)); | ||
}; | ||
|
||
return ( | ||
<div | ||
css={css` | ||
display: flex; | ||
justify-content: center; | ||
flex-direction: column; | ||
align-items: center; | ||
width: 100%; | ||
height: 75%; | ||
border: 0.0625rem solid red; | ||
background-color: black; | ||
`} | ||
> | ||
면접페이지의 카메라 입니다. | ||
<video | ||
ref={mirrorVideoRef} | ||
playsInline | ||
autoPlay | ||
muted | ||
css={css` | ||
width: 100%; | ||
height: 80%; | ||
transform: scaleX(-1); | ||
`} | ||
/> | ||
|
||
<div> | ||
<button onClick={handleStartRecording} disabled={recording}> | ||
시작 | ||
</button> | ||
<button onClick={handleStopRecording} disabled={!recording}> | ||
종료 | ||
</button> | ||
<button | ||
onClick={handleDownload} | ||
disabled={recording || recordedBlobs.length === 0} | ||
> | ||
저장 | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default InterviewCamera; |