프로젝트 '재움' 개발과정(3)
안녕하세요! 오늘은 촬영정보를 넣어보는 시간을 가지도록 해보겠습니다.
그전에 먼저 html파일을 css로 꾸며보도록 하겠습니다.
home.html
<!DOCTYPE html>
<html>
<head>
<title>메인 화면</title>
<link rel="stylesheet" type="text/css" href="static/css/home.css" />
</head>
<body>
<div class="container">
<h1>재움</h1>
<p>사용자를 생성하거나 기존 사용자로 로그인하세요.</p>
<div class="input-container">
<input
name="nickname"
id="nickname"
placeholder="닉네임을 입력해주세요"
/>
</div>
<div class="button-container">
<button id="saveButton">새로운 사용자 생성</button>
<button id="capButton">촬영페이지로</button>
</div>
</div>
<script src="static/js/home.js"></script>
</body>
</html>
home.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 400px;
background-color: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 5px;
text-align: center;
}
h1 {
color: #333;
}
p {
color: #666;
margin-bottom: 20px;
}
.button-container {
text-align: center;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
.input-container {
text-align: center;
margin: 20px 0;
}
input[name="nickname"] {
margin: 0 auto;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
text-align: center; /* 입력 텍스트를 가운데 정렬합니다. */
}
input[name="nickname"]::placeholder {
color: #999; /* 플레이스홀더 텍스트 색상 변경 */
}
그대로 넣어주시면
이렇게 변합니다.
record 화면도 바꿔주도록 하겠습니다.
record.html (audio 부분은 나중에 다루도록 하겠습니다.)
<!DOCTYPE html>
<html>
<head>
<title>웹 카메라 촬영</title>
<link rel="stylesheet" type="text/css" href="/static/css/record.css" />
</head>
<body>
<audio id="backgroundMusic" controls autoplay>
<source src="static/music/{{randomMusic}}" type="audio/mpeg" />
</audio>
<div class="container">
<h1>수면 측정</h1>
<!-- 닉네임 입력칸 -->
<input
type="hidden"
id="nickname"
name="nickname"
value="{{ nickname }}"
/>
<input type="hidden" id="sleep_info_id" value="{{ sleep_info_id }}" />
<!-- 웹캠 -->
<video id="camera" autoplay></video>
<div class="button-container">
<button id="captureButton">촬영</button>
<button id="endButton">종료</button>
</div>
</div>
<script src="static/js/record.js"></script>
</body>
</html>
record.css
/* styles.css */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 5px;
}
#backgroundMusic {
display: none;
}
h1 {
text-align: center;
color: #333;
}
input[type="hidden"] {
display: none;
}
video {
display: block;
margin: 0 auto;
max-width: 100%;
}
.input-container {
text-align: center;
margin-top: 20px;
}
input[name="nickname"] {
margin: 0 auto;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
.button-container {
text-align: center;
margin-top: 20px;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
그대로 넣어주시면
이렇게 변합니다.
촬영정보를 db에 저장할 때 닉네임이 필요하기 때문에
home.js에 닉네임을 입력하지 않으면 촬영페이지에 넘어가지 않도록 수정하겠습니다.
home.js (이전 코드에서 capButton 이벤트리스너 부분만 수정하였습니다.)
document.getElementById("capButton").addEventListener("click", function () {
const nickname = document.getElementById("nickname").value;
// 닉네임이 비어 있는 경우 경고 메시지 표시하고 리다이렉트를 실행하지 않음
if (nickname.trim() === "") {
alert("닉네임을 입력해주세요.");
} else {
// 리다이렉트 URL을 생성하여 /record?nickname=에 닉네임을 추가합니다
const redirectURL = "/record?nickname=" + nickname;
// 페이지를 리다이렉트합니다
window.location.href = redirectURL;
}
});
닉네임을 입력하지 않고 넘어가려고 하면 alert 메시지가 나타납니다.
닉네임 입력 후 촬영페이지로 넘어가서 촬영을 누르게 되면 웹캠이 실행됩니다.
record.js에 작성해주었습니다.
// HTML 문서에서 camera id를 가진 비디오 요소를 가져옵니다.
const videoElement = document.getElementById("camera");
// captureButton id를 가진 버튼 요소를 가져옵니다.
const captureButton = document.getElementById("captureButton");
// endButton id를 가진 버튼 요소를 가져옵니다.
const endButton = document.getElementById("endButton");
let mediaStream; // 비디오 스트림 객체
let captureStartTime; // 촬영 시작 시간
let captureEndTime; // 촬영 종료 시간
let intervalId; // Interval 핸들러 ID를 저장할 변수
captureButton.addEventListener("click", function () {
// 웹 카메라에 액세스하여 비디오 스트림 가져오기
navigator.mediaDevices
.getUserMedia({ video: true })
.then(function (stream) {
mediaStream = stream; // 비디오 스트림 저장
videoElement.srcObject = stream;
captureStartTime = new Date(); // 촬영 시작 시간 저장
const startTime = new Date(captureStartTime);
const startSleepTime = formatTime(startTime);
const urlParams = new URLSearchParams(window.location.search);
const nickname = urlParams.get("nickname"); // URL에서 닉네임 가져오기
// 데이터를 JSON 형식으로 준비
const sleepInfoData = {
nickname: nickname,
start_sleep: startSleepTime,
};
console.log(JSON.stringify(sleepInfoData));
// 데이터를 JSON 문자열로 변환
const sleepInfoJSON = JSON.stringify(sleepInfoData);
// 서버로 닉네임과 촬영시작시간 데이터를 JSON 형태로 전송
fetch("/record/create_sleep_info", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: sleepInfoJSON,
})
.then(function (response) {
// 서버 응답이 JSON 형식일 경우
return response.json();
})
.then(function (data) {
// JSON 데이터를 사용할 수 있습니다.
console.log("서버에서 받은 데이터:", data);
})
.catch(function (error) {
console.error("Failed to create sleep info:", error);
});
// 10분 후부터 프레임 전송 시작
// timeoutHandlerId = setTimeout(captureAndUploadFrame, 600000);
intervalId = setInterval(captureAndUploadFrame, 10000);
})
.catch(function (error) {
console.error("웹 카메라 액세스 오류:", error);
});
});
촬영이 시작되고 10초마다 captureAndUploadFrame 함수가 사용됩니다.
captureAndUploadFrame함수는 이미지를 저장할 때 작성하도록 하겠습니다.
crud.py에 수면정보 저장을 위한 함수를 작성하겠습니다.
crud.py (저번에 이어 작성합니다.)
def create_db_sleep_info(db: Session, sleep_info: schemas.SleepInfoBase):
db_sleep_info = model.SleepInfo(
nickname=sleep_info.nickname,
start_sleep=sleep_info.start_sleep,
)
print(type(sleep_info.nickname))
db.add(db_sleep_info)
db.commit()
db.refresh(db_sleep_info)
return db_sleep_info
api_record.py
# 수면 정보 생성
@router.post("/record/create_sleep_info", response_model=SleepInfoBase)
async def create_sleep_info(sleep_info: SleepInfoBase, db: Session = Depends(get_db) ):
db_sleep_info = create_db_sleep_info(db, sleep_info)
return db_sleep_info
두 함수를 통해 db에 닉네임과 촬영시작시간이 먼저 저장됩니다.
이제 종료를 눌렀을 때 종료시간과 총 촬영시간을 업데이트하도록 하겠습니다.
record.js
// "종료" 버튼 클릭 시 비디오 중지 및 촬영 종료 시간 저장
endButton.addEventListener("click", function () {
if (mediaStream) {
mediaStream.getTracks().forEach(function (track) {
track.stop(); // 비디오 스트림 중지
});
videoElement.srcObject = null; // 비디오 정지
captureEndTime = new Date(); // 촬영 종료 시간 저장
if (timeoutHandlerId) {
clearTimeout(timeoutHandlerId);
}
// 촬영 중지를 위해 Interval을 제거
if (intervalId) {
clearInterval(intervalId);
}
const endTime = new Date(captureEndTime);
const endSleepTime = formatTime(endTime);
// 촬영 시작 및 종료 시간을 초로 변환
const startTimeInSeconds = Math.floor(captureStartTime.getTime() / 1000);
const endTimeInSeconds = Math.floor(captureEndTime.getTime() / 1000);
// total_sleep 계산
const totalSleepInSeconds = endTimeInSeconds - startTimeInSeconds;
const totalSleepTime = secondsToHMS(totalSleepInSeconds);
// 데이터를 JSON 형식으로 준비
const urlParams = new URLSearchParams(window.location.search);
const nickname = urlParams.get("nickname"); // URL에서 닉네임 가져오기
const sleepInfoData = {
nickname: nickname,
end_sleep: endSleepTime,
total_sleep: totalSleepTime,
};
console.log(JSON.stringify(sleepInfoData));
// 데이터를 JSON 문자열로 변환
const sleepInfoJSON = JSON.stringify(sleepInfoData);
// 서버로 닉네임, 촬영종료시간, 총 시간 데이터를 JSON 형태로 전송
fetch(`/record/update/${nickname}`, {
method: "PUT", // PUT 메서드 사용
headers: {
"Content-Type": "application/json",
},
body: sleepInfoJSON,
})
.then(function (response) {
if (response.ok) {
console.log("촬영 정보가 서버로 전송되었습니다.");
} else {
console.error("촬영 정보 전송 실패", response);
}
})
.catch(function (error) {
console.error("촬영 정보 전송 중 오류 발생:", error);
});
}
});
db업데이트를 위해 crud.py에 함수를 추가합니다.
def update_info(db: Session, nickname: str, total_sleep: str, end_sleep: str):
# 가장 마지막 sleep_info_id 가져오기
latest_sleep_info_id = db.query(func.max(model.SleepInfo.sleep_info_id)).filter(model.SleepInfo.nickname == nickname).scalar()
if latest_sleep_info_id is not None:
# 해당 sleep_info_id를 사용하여 업데이트
db_sleep_info = db.query(model.SleepInfo).filter(model.SleepInfo.sleep_info_id == latest_sleep_info_id).first()
if db_sleep_info:
db_sleep_info.total_sleep = total_sleep
db_sleep_info.end_sleep = end_sleep
db.commit()
db.refresh(db_sleep_info)
return db_sleep_info
api_record.py
# 수면 정보 업데이트
@router.put("/record/update/{nickname}", response_model=SleepInfoUpdate)
def update_sleep_info(nickname: str, sleep_info_data: SleepInfoUpdate, db: Session = Depends(get_db)):
updated_sleep_info = update_info(db, nickname, sleep_info_data.total_sleep, sleep_info_data.end_sleep)
return updated_sleep_info
이제 종료버튼을 누르게 되면
나머지 칼럼이 업데이트되는 것을 확인해 볼 수 있습니다.
다음 시간엔 클라이언트에서 보낸 이미지를 서버로 보내 자세를 추출하고 비교하는 것과 자세가 3번 연속으로 바뀌면 자세가 변화한 거로 인식하여 수면이벤트 db에 저장하는 것을 해보겠습니다.