video & videoControls - CSS μ€νμΌλ§ λ° μ λ ¬
keydown μ΄λ²€νΈ
API - apiRouter / sendStatus / fetch API / data attribute
CSS μ€νμΌλ§ λ° μ λ ¬ + video player λ§λ¬΄λ¦¬
λ§νλ€κ° ν΄κ²°ν λΆλΆλ€ νΉμ λ€μ λ³΄κ³ μΆμ λΆλΆλ€ μ 리
videoλ inline λ 벨 μμμ΄λ€.
μ¬νκΉμ§ block λ 벨 μμλ‘ μκ³ μμλ κ±° μ€νμΈκ°. νκ΅μ΄λ‘ ꡬκΈλ§νλ©΄ ν¬μ€ν
λ§λ€ videoκ° block λ 벨 μμλΌκ³ μ€λͺ
λμ΄ μλ€. MDNμμ νμΈν΄λ³΄λ inline λ 벨 μμλΌκ³ μ ν μμλ€.
video {
display: block;
margin: 0 auto;
}
ν°λ°μ΄ν μμλ μμλ°μ΄νλ₯Ό μ¨μΌ νλ€. μμ§ λ§μ.
'showing' classκ° μμ λλ§ λΉλμ€ μ»¨νΈλ‘€λ¬κ° 보μ΄λλ‘ ν΄μΌ νλ€.
κ·Έλ¬λ, μλμ²λΌ μμ±νλ©΄ λΉλμ€ μ»¨νΈλ‘€λ¬λ μ΄λ»κ² ν΄λ 보μ΄μ§ μλλ€.
#videoControls {
opacity: 0;
}
.showing {
opacity: 1;
}
μ΄λ κ² μμ±ν΄μΌ νλ€.
#videoControls {
opacity: 0;
&.showing {
opacity: 1;
}
}
watch.pug νμΌκ³Ό videoPlayer.js νμΌμμ λ²νΌ μμ ν
μ€νΈλ€μ font-awesome μμ΄μ½μΌλ‘ λ°κΏ¨λ€.
videoContainer(video & videoControls)λ₯Ό μ€νμΌλ§νκ³ μ λ ¬νλ€.
#videoContainer {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
video {
width: 100%;
max-height: 800px; // μΈλ‘κ° κΈ΄ λμμμ μν΄ μΆκ°ν¨
}
#videoControls {
opacity: 0;
&.showing {
opacity: 1;
}
display: flex;
justify-content: space-between;
align-items: center;
width: 98%;
padding-bottom: 1%;
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -100%);
background-color: transparent;
button {
all: unset;
cursor: pointer;
}
#volume {
width: 10%;
&:focus {
outline: none;
}
}
#timeline {
width: 65%;
&:focus {
outline: none;
}
}
}
π‘ ν€λ³΄λ ν€λ₯Ό λλ₯΄λ©΄
keydown β keypress β keyup
μ μμλλ‘ μ΄λ²€νΈκ° λ°μνλ€.μ΄λ keydown μ΄λ²€νΈλ ν€λ³΄λμ λͺ¨λ ν€λ₯Ό λλ μ λ λ°μνλ λ°λ©΄, keypress μ΄λ²€νΈλ μΆλ ₯ν μ μλ ν€λ³΄λλ₯Ό λλ μ λλ§ λ°μνλ€.(νμ΄ν λ±μ λλ μ λλ keypress μ΄λ²€νΈ λ°μx)
playBtnκ³Ό videoλ₯Ό ν΄λ¦νκ±°λ Space
λ°λ₯Ό λλ₯΄λ©΄, videoκ° μ¬μ λλ μΌμμ μ§λλλ‘ μμ νλ€.
space λ°λ₯Ό λλ₯Ό λλ§λ€ μ€ν¬λ‘€μ΄ λ΄λ €κ°λ κ²(λΈλΌμ°μ μ κΈ°λ³Έ λμ)μ λ§κΈ° μν΄ event.preventDefault()
λ₯Ό μ¬μ©νλ€.
const handlePlayOrPause = () => {
if (video.paused) {
video.play();
} else {
video.pause();
}
playBtn.innerHTML = video.paused
? "<i class='fas fa-play'></i>"
: "<i class='fas fa-pause'></i>";
};
const handleKeyPress = (event) => {
if (event.code === "Space") {
handlePlayOrPause();
event.preventDefault()
}
};
playBtn.addEventListener("click", handlePlayOrPause);
video.addEventListener("click", handlePlayOrPause);
window.addEventListener("keypress", handleKeyPress);
ArrowLeft
μ ArrowRight
λ₯Ό λλ₯΄λ©΄ λμμ νμ¬ μ¬μ μμΉλ₯Ό μ΄λν μ μλλ‘ μμ νλ€.
handleKeyPress ν¨μλ₯Ό swith ꡬ문
κ³Ό event.key
λ₯Ό μ΄μ©ν΄ μμ±νλ€.
const handleKeyPress = (event) => {
const { key } = event;
const { currentTime } = video;
switch (key) {
case " ":
handlePlayOrPause();
event.preventDefault();
break;
case "ArrowLeft":
video.currentTime = currentTime - 1;
break;
case "ArrowRight":
video.currentTime = currentTime + 1;
}
};
window.addEventListener("keydown", handleKeyPress);
μ 체 νλ©΄ λ²νΌμ ν΄λ¦νμ λλΏ μλλΌ Enter
ν€λ₯Ό λλ¬λ μ 체 νλ©΄μΌλ‘ μ νν μ μλλ‘ μμ νλ€.
const handleFullscreen = () => {
const fullscreen = document.fullscreenElement;
if (fullscreen) {
document.exitFullscreen();
fullScreenBtn.innerHTML = "<i class='fas fa-expand'></i>";
} else {
videoContainer.requestFullscreen();
fullScreenBtn.innerHTML = "<i class='fas fa-compress'></i>";
}
};
const handleKeyPress = (event) => {
const { key } = event;
const { currentTime } = video;
switch (key) {
case " ":
handlePlayOrPause();
event.preventDefault();
break;
case "ArrowLeft":
video.currentTime = currentTime - 1;
break;
case "ArrowRight":
video.currentTime = currentTime + 1;
break;
case "Enter":
handleFullscreen();
break;
}
};
fullScreenBtn.addEventListener("click", handleFullscreen);
window.addEventListener("keydown", handleKeyPress);
μμ λ°±μλλ₯Ό λ€λ£° λ pugλ₯Ό μ¬μ©ν΄μ NodeJSλ‘ ν
νλ¦Ώ(views)μ λ λλ§νλ SSR λ°©μμ μ¬μ©νλ€.
SSR(Server Side Rendering)
μ΄λ μλ²κ° ν
νλ¦Ώμ λ λλ§νλ μΌκΉμ§ νλ κ²μ μλ―Ένλ€.
κ·Έλ¬λ, λ°±μλμμ ν
νλ¦Ώμ λ λλ§νμ§ μκ³ μμ¦μ APIλ₯Ό μ΄μ©νλ€.
API
λ λ°±μλκ° ν
νλ¦Ώμ λ λλ§νμ§ μμ λ νλ‘ νΈμλμ λ°±μλκ° μλ²λ₯Ό ν΅ν΄ ν΅μ νλ λ°©λ²μ λ§νλ€.
userκ° video μμ²μ νλ² μλ£ν λλ§λ€, μ‘°νμκ° μ λ°μ΄νΈλλλ‘ APIλ₯Ό μ΄μ©ν΄ ꡬνν΄λ³΄λ €κ³ νλ€.
apiRouter.js νμΌμ λ§λ ν apiRouterλ₯Ό λ§λ€μ΄ export default νλ€.
server.js νμΌμμ apiRouterλ₯Ό import ν ν μ΄λ₯Ό μ¬μ©ν μ μλλ‘ μΆκ°νλ€.
// apiRouter.js
import express from "express";
const apiRouter = express.Router();
export default apiRouter;
// server.js
import apiRouter from "./routers/apiRouter";
app.use("/api", apiRouter);
apiRouter.js νμΌμμ routeλ₯Ό μΆκ°νλ€.
userκ° videoλ₯Ό μμ²νκΈ° μμνλ©΄ λ°±μλμ post μμ²μ 보λ΄λλ‘ νλ€.
// apiRouter.js
import { registerView } from "../controllers/videoController";
apiRouter.post("/videos/:id([0-9a-f]{24})/view", registerView);
registerView 컨νΈλ‘€λ¬λ₯Ό μμ±νλ€.
req.params.idλ₯Ό μ΄μ©ν΄ DBμμ videoλ₯Ό μ°Ύμ ν video.meta.viewsλ₯Ό μ
λ°μ΄νΈνλ€.
μ΄λ urlμ λ°κΏ νμ΄μ§λ₯Ό μ΄λνμ§λ μμ κ²μ΄λ€. (μ¦, ν
νλ¦Ώμ λ λλ§νμ§ μμ κ²μ΄λ€.)
κ·Έλ¬λ ν
νλ¦Ώμ λ λλ§νμ§ μμλ μλ²κ° μμ²μ λν μλ΅μ ν΄μΌ νλ―λ‘ render λμ sendStatus
λ₯Ό μ΄μ©ν΄ status codeλ₯Ό 보λ΄λλ‘ νλ€.
π‘ statusλ render μ μ μν μ½λλ₯Ό μ§μ ν΄μ€ λΏμ΄μ§λ§, sendStatusλ μ§μ ν μν μ½λλ₯Ό 보λμΌλ‘μ¨ μ°κ²°μ λλ΄λ²λ¦°λ€.
// videoController.js
export const registerView = async (req, res) => {
const { id } = req.params;
const video = await Video.findById(id);
if (!video) {
return res.sendStatus(404);
}
video.meta.views = video.meta.views + 1;
await video.save();
return res.sendStatus(200);
};
π‘ μν μ½λ
μν μ½λλ κΈ°λ₯μ΄ μ λλ‘ μ²λ¦¬λμλμ§λ₯Ό μλ €μ€λ€.
μ‘°νμλ₯Ό μ λ°μ΄νΈν λλ μν μ½λλ₯Ό μ§μ ν΄μ£ΌκΈ΄ νμ§λ§ μ΄λ₯Ό κ΅³μ΄ μ΄μ©νμ§λ μμ κ²μ΄λ€. μ¬μ©μμκ² 'μ‘°νμκ° μ¬λΌκ°μ§ μμμ΅λλ€.'λΌκ³ μλ €μ€ νμλ μκΈ° λλ¬Έμ΄λ€.
κ·Έλ¬λ, λ€μμ λκΈμ λ€λ£° λλ μν μ½λλ₯Ό μ΄μ©ν κ²μ΄λ€. μ¬μ©μμκ² 'λκΈ μμ±μ μ€ν¨νμ΅λλ€.'λΌκ³ μλ €μ£Όκ±°λ μΆκ°λ λκΈμ λ³Ό μ μλλ‘ λ§λ€μ΄μ€μΌ νκΈ° λλ¬Έμ΄λ€.
μμμ μ‘°νμλ₯Ό μ
λ°μ΄νΈνλ μ½λλ₯Ό μμ±νλ€.
μ΄μ userκ° video μμ²μ νλ² μλ£ν λλ§λ€ νλ‘ νΈμλμμ μμμ μμ±ν urlμ νΈμΆνλλ‘ ν΄μΌ νλ€.
μμ λ°±μλλ₯Ό λ€λ£° λλ λΈλΌμ°μ κ° μλ²μ μμ²μ νμ¬ urlμ΄ λ°λλ©΄(νμ΄μ§κ° μ΄λλλ©΄) μλ²κ° μλ΅μ νλ λ°©μμ΄μλ€.
μ΄λ²μλ νλ‘ νΈμλμμ νμ΄μ§ μ΄λ μμ΄ urlμ νΈμΆν΄λ³Ό κ²μ΄λ€.
μ΄λ₯Ό ν΅ν΄ urlμ΄ λ°λμ§ μμλ νμ΄μ§μ λ³νκ° μΌμ΄λλ interactive websiteλ₯Ό λ§λ€ μ μλ€.
// videoPlayer.js
const handleVideoEnded = () => {
// μ€λ΅
// fetch("/api/videos/videoμ id/view");
};
video.addEventListener("ended", handleVideoEnded);
λ¨Όμ , λΉλμ€κ° λλλ κ²μ κ°μ§νλ ended μ΄λ²€νΈλ₯Ό λ±λ‘ν ν μ΄λ²€νΈ 리μ€λμμ fetch()λ₯Ό μ΄μ©ν΄ μμμ λ§λ routeμ μμ²μ 보λ΄λλ‘
νλ €κ³ νλ€.
κ·Έλ°λ° apiλ‘ μμ²μ 보λ΄κΈ° μν΄μλ videoPlayer.jsμμ video.idμ κ°μ κ°μ Έμ¬ μ μμ΄μΌ νλ€.
μ΄λ₯Ό μν΄ data μμ±
μ μ΄μ©ν΄ μ§μ 컀μ€ν
λ°μ΄ν°λ₯Ό λ§λ€μ΄ HTML elementμ μ μ₯νλ €κ³ νλ€.
νλ‘ νΈμλμμ μλ°μ€ν¬λ¦½νΈκ° μ μ μλλ‘ pug νμΌμ video.idμ κ°μ λ¨κΈΈ μ μλ€.
//- watch.pug
// μ€λ΅
video(src="/" + video.fileUrl, data-id=video._id)
pug νμΌμμ data-
λ₯Ό μ΄μ©ν΄ 컀μ€ν
λ°μ΄ν°λ₯Ό μ μ₯νλ€.
// videoPlayer.js
const handleVideoEnded = () => {
// μ€λ΅
const { id } = video.dataset;
fetch(`/api/videos/${id}/view`, {
method: "POST",
})
};
video.addEventListener("ended", handleVideoEnded);
νλ‘ νΈμλμμ element.dataset
μ μ΄μ©ν΄ μ μ₯ν λ°μ΄ν°λ₯Ό κ°μ Έμ¨λ€.
fetch()λ κΈ°λ³Έμ μΌλ‘ GET Requestλ₯Ό 보λ΄κ² λλ―λ‘ μ΄λ₯Ό POST Requestλ‘ λ°κΏμΌ νλ€.
μ΄μ μ무κ²λ ν΄λ¦νμ§ μκ³ urlμ λ°κΎΈμ§ μμλ μ§μ ν μ΄λ²€νΈκ° λ°μνκΈ°λ§ νλ©΄ νλ‘ νΈμλμμ μλ°μ€ν¬λ¦½νΈκ° μμμ λ°±μλμ μμ²μ 보λΈλ€.