와! 정보처리기사 결과가 이번 주에 나왔다. 결과는 예상한대로 합격! 🙌🙌🙌 아직 실기를 취득한건 아니지만 첫 단추를 잘 끼웠다는 의미에서 나 자신 칭찬한다. 4월 중에 실기 시험이 있다. 주말마다 공부해서 한번에 합격할 수 있도록 해보겠다. 어느덧 3월도 중순이다. 3월의 셋째 주를 되돌아본다.
테스트 코드 고도화 예정
RestDocs -> mockMvc test
UtillClass -> mockito test
Repository -> DataJpaTest
Service, controller -> SpringBootTest
@Component
public class VideoStreamingUtil {
public VideoStreamingResponse resolve(Video video, HttpServletRequest request) {
// Range 헤더 파싱
String rangeHeader = request.getHeader("Range");
long start;
long end = video.getVideoSize() - 1;
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {
String[] ranges = rangeHeader.substring("bytes=".length()).split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
}
if (end >= video.getVideoSize()) {
end = video.getVideoSize() - 1;
}
} else {
start = 0;
}
long contentLength = end - start + 1;
// Response 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("video/mp4"));
headers.add("Accept-Ranges", "bytes");
headers.setContentLength(contentLength);
if (rangeHeader != null) {
headers.set("Content-Range", String.format("bytes %d-%d/%d", start, end, video.getVideoSize()));
}
StreamingResponseBody streamingResponseBody = createStreamingResponse(video, start, contentLength);
// Range 요청이 있으면 206, 없으면 200 반환
HttpStatus status = (rangeHeader != null) ? HttpStatus.PARTIAL_CONTENT : HttpStatus.OK;
return new VideoStreamingResponse(streamingResponseBody, status, headers);
}
// 스트리밍 바디 생성
private StreamingResponseBody createStreamingResponse(Video video, long start, long contentLength) {
return outputStream -> {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(new File(video.getVideoPath()), "r")) {
randomAccessFile.seek(start);
byte[] buffer = new byte[1024];
long bytesToRead = contentLength;
while (bytesToRead > 0) {
int bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead));
if (bytesRead == -1) break;
outputStream.write(buffer, 0, bytesRead);
bytesToRead -= bytesRead;
}
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException("Video streaming failed", e);
}
};
}
}
@Component
public class UserUtil {
public User buildUserWith(UserSignUpRequest userSignUpRequest, PasswordEncoder passwordEncoder) {
return User.builder()
.name(userSignUpRequest.username())
.email(userSignUpRequest.email())
.password(passwordEncoder.encode(userSignUpRequest.password()))
.userRole(USER)
.build();
}
public String saveImage(MultipartFile profileImage, User user, UserImageType userImageType) {
if (!Objects.isNull(userImageType.getImagePathOf(user))) {
deleteImage(userImageType.getImagePathOf(user));
}
String basePath = "myVideos/" + user.getId() + "/images/" + userImageType.getFolderName();
String fileName = UUID.randomUUID() + "_" + profileImage.getOriginalFilename();
Path filePath = Paths.get(basePath, fileName);
try {
Files.createDirectories(filePath.getParent());
Files.write(filePath, profileImage.getBytes());
} catch (IOException e) {
throw new RuntimeException("프로필 사진 변경에 실패했습니다.", e);
}
return filePath.toString();
}
private void deleteImage(String imagePath) {
Path path = Paths.get(imagePath);
try {
Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException("이미지 삭제에 실패했습니다.", e);
}
}
}