과정/2차 AI프로젝트 서비스 개발

프로젝트 '재움' 개발과정(3)

줘요 2023. 10. 23. 23:56

안녕하세요! 오늘은 촬영정보를 넣어보는 시간을 가지도록 해보겠습니다.

 

그전에 먼저 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에 저장하는 것을 해보겠습니다.