본문 바로가기
Project

[해커톤][호남의 얼굴]얼굴인식 출석 서비스

by SolaBreeze 2023. 9. 4.

본 프로젝트는 오픈 소스로 공개해두었습니다🤭

👇🏻 클릭해주세용 👇🏻

깃허브 주소 <-----[클릭!]

 

GitHub - DYSA-OS/face-recognition-attendance-system: manage student's attendance(face recognition) and grades

manage student's attendance(face recognition) and grades - GitHub - DYSA-OS/face-recognition-attendance-system: manage student's attendance(face recognition) and grades

github.com

 

대회가 이루어지는 "홀리데이인 호텔" 도착~!

<2023년 08월 15일 - 8월 31일> 약 보름간 개발이 진행되는 해커톤 대회였다.

이번에는 내가 두번째로 참가하는 해커톤 대회였다.
저번 해커톤때는 장고 백엔드를 맡았는데, 계획한 모든 기능들을 구현하는데 성공하긴 했지만 깊은 지식이 부족했고 장고를 쓰는 백엔드 개발자가 별로 없어서 협업하기가 힘들다는 것을 느꼈었다. 그래서 대회 이후 내 백엔드 실력의 부족함도 채우고, 다른 사람들과 협업할때 용이하기 위해 약 한달간 스프링(부트)에 대해 공부하는 시간을 가졌다.
한달간 공부하면서 스프링이란 기술에 대해 모든것을 알진 못했지만, 그동안 내가 배운 기본적인 기술들을 활용해서 일단 실전 프로젝트를 해보는 경험이 중요하다고 생각했다!
그래서 이번 대회에서는 스프링을 이용해서 백엔드를 구성할 예정이다!! 저번보다 더욱 발전했다고오~~~~🔥🔥🔥🔥🔥

 

우리팀 이름은 김강정이다~ (tmi :&nbsp; 김씨1, 강씨2, 정씨1)

 

팀은 기획자 1명, AI 모델 개발자 1명, 백엔드 개발자 1명, 디자이너 1명으로 구성되었다.

AI 모델인 개발자 "다영"이와 백엔드 개발자인 "나"는 얼굴인식 모델을 구현하고, 해당 모델을 이용하여 웹 형태로 제공할 수 있는 서비스를 만들어보고 싶었다. 그래서 우리 주변에서 흔히 볼 수 있는 학사관리 프로그램을 접목시켜 얼굴 인식을 통한 출결 인식을 할 수 있는 기능이 있는 "학사 관리" 웹 서비스를 만들어보기로 했다. 

아쉽지만 대회 특성상 개발자가 최대 2명으로 구성되야했었다... 이번엔 스프링을 사용하는 백엔드 개발자와 협업해보고 싶었지만 이번 대회는 과동기인 "다영"이(AI모델 개발)와 함께 팀을 하고자 했기 때문에...

또 문제점이 있다. 우리는 프론트엔드 개발자를 데려올 수 없다는 것이다!!ㅠㅠ
결국 우리 팀은 프론트엔트 개발자 없이, 내가 그냥 부트스트랩을 이용하여 간단한 프론트를 구성하기로 했다...(템플릿 구성은 다영이가 좀 도와주기로 함(다행😭))

 

👩🏻‍🏫  서비스 기능 ⬇️⬇️

👇🏻 클릭해주세용 👇🏻

더보기

1) 클래스 및 학생 등록 Flow

  • [교수님] 회원가입 진행 (호남권 대학 관련자인지 인증) → 학교 입력 (호남권 학교 리스트)
    • 이름
    • 이메일
    • 학교 입력 (라디오 버튼)
    • 비밀번호
    • 비밀번호 확인
    • 사진 등록
  • [교수님] 로그인
    • 이메일
    • 비밀번호
  • [교수님] 클래스 개설
    • 학기 선택
    • 클래스 선택
  • [대학생] 회원가입
    • 이름
    • 이메일
    • 비밀번호
    • 비밀번호 확인
    • 학교 입력 (드롭박스)
    • 전공 입력
    • 학번 입력
    • 사진 등록(추후 출석체크를 위한 데이터)
  • [교수님] 학생 등록
    • 클래스에 학생 등록 (학번)

2) 출결 시스템 진행 Flow

  • [교수님] 출석 시작
    • 출석 시작 버튼을 눌러 출석을 시작함.
    • 출석 종료 버튼을 눌러야 출석이 종료가 완료 됨.
  • [대학생] 한 명씩 나와서 교수님 노트북 웹 캠을 이용해 출석 확인
    • 얼굴 인식 → 유저 식별 → 출석 완료
  • [교수님] 출석자, (지각자), 결석자 확인 가능
    • 출석자의 경우 출석 시간으로 오름차순 정렬
    • 시간내에 출석체크를 하지 못한 학생은 결석으로 처리된다.

3) 출결 시스템 진행 Flow (추후)

  • [교수님] 출석 시작
    • 출석 시작 버튼을 눌러 출석을 시작함.
    • 출석 종료 버튼을 눌러야 출석이 종료가 완료 됨.
      • 출석을 시작하면 QR코드가 발급이 됨.
  • [대학생] 휴대폰 카메라를 통해 QR코드로 웹사이트로 진입
    • 얼굴 인식 → 유저 식별 → 출석 완료
  • [교수님] 출석자, 지각자, 결석자 확인 가능
    • 출석자의 경우 출석 시간으로 오름차순 정렬
    • 결석자 옆엔 지각 버튼이 있는데, 지각 버튼을 누르면 지각자로 변경할 수 있음 (출석 시간은 지각 버튼을 눌렀을 때로 설정됨)

4) 성적 기입 시스템

  • [교수님] 성적 기입
    • 성적 기입 버튼을 눌러 성적 기입을 시작함
    • 각 학생들 마다 성적을 입력할 수 있는 리스트가 나열되어 있고, 성적은 드롭박스를 통해 기입

5) 학생의 출결 및 성적 조회 시스템

  • [대학생] 출결 및 성적 조회
    • 학생 페이지에서 조회를 원하는 과목에 ‘강의실 입장’ 버튼 클릭
    • 해당 과목에 대한 성적과 출석이 리스트 형태로 나열되어 있음(조회 가능)

6) 출석 통계 시스템

  • 출석 통계
    • 지금까지의 출석 통계를 확인할 수 있음
    • 출석, 지각, 결석 3가지로 확인 가능

7) 형성 평가 시스템

  • [교수님] 교수님은 다양한 방식으로 학생들에게 형성 평가를 진행할 수 있음
    • O, X 퀴즈 (개발)
    • 4지선다 객관식 퀴즈

8) 발표자 뽑기 시스템

  • 출석한 학생 중 랜덤으로 한 명을 뽑는다.

 

 

 

📌 AI 모델 

 

- 언어 : python
- IDE : pycharm
- use : face recogntion, svm

객체 당 단일이미지의 데이터셋을 통해 모델이 학습되기 때문에, 다른 모델 대비 데이터 수집, 관리, 활용면에서 유리하다.

 

 

📌 WEB 서비스 개발

<백엔드>
- 언어: java17

- 프레임워크: SpringBoot 3.1.3
      - package: jar
- 데이터베이스: h2
- IDE: intellij 

<프론트엔드>
- html, css, js

- Thymeleaf
- 테마 : Bootstrap

 

도메인 모델 분석

  • 객체(엔티티)들을 일단 미리 설정을 해놓고(무슨 객체가 필요한지) 다대다매핑, 혹은 다대일매핑.
  • STUDENT - COURSE 는 다대다 매핑
    • But, 데이터베이스 상의 오류를 예방하기 위해 일대다, 다대일매핑으로 중간엔티티인 StudentCourse를 설정

 

- 스프링 MVC 패턴 적용
controller: 웹계층
service: 비즈니스로직
repository: JPA 직접사용 계층(ex. createQuery), em 사용
domain: 엔티티가 모여있는 계층 => 모든 계층에서 사용!

- 개발 순서 
entity(domain) 개발 👉🏻 repository 개발 👉🏻 service 개발 👉🏻 tc 검증 👉🏻 controller 개발

- 모든 코드는 DI, IOC 원칙을 따르며 작성

 

⭐️ 수 많은 오류들과 시행착오..👀(흑흑.. 꼭 봐주세요)⬇️

1.  프론트의 경우 타임리프 문법을 통해 html로 구성했는데, 이미지가 제대로 나오지 않는 문제가 발생!!

👇🏻 클릭해주세용 👇🏻

더보기
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // addResourceHandler : 스프링부트에서 확인할 경로 설정
        // addResourceLocations : 실제 시스템의 폴더 위치 설정

        // 아래 경로를 해당 폴더의 경로로 수정 해야된다.
        registry.addResourceHandler("/img/**").addResourceLocations("file:///Users/sola/Documents/GitHub/FaceAutoAttendence/data/");
    }
}

ㄴ Application이 있는 폴더에 Configuration을 만들면 된다.

여기서 정말정말 중요한게, addResourceLocations에서

 ⭐️⭐️⭐️  file:/// 경로 /

꼭 이런 방식으로 넣어줘야 인식한다!! (마지막에 / 닫는거 주의!!!!!!!) → 이거때문에 죙일 고생함

그렇게 되면

타임리프에서 사용

<img src="/img/logo.png">
<img id="profilePicture" th:if="${professor.profilePicture}" th:src="@{/img/} + ${professor.getProfilePicture()}" alt="Profile Picture">

이런식으로 html에서 사용했을때 외부 경로에서 잘 읽어들이게 된다.

위에 configuration에서 addResourceHandler("/img/")로 설정했기 때문에 경로가 “/img/”인 모든 파일에 대해서 성립한다.

!! 여기서 왜 “외부 경로에 이미지 파일을 넣어놓고 이를 경로로 불러오기”로 사용하느냐 !!

  • 첫번째 이유 : 이미지의 업로드와 불러오기가 실시간으로 반영되어야하기 때문에!!!
    • 이 이유 때문에 내가 이 방법을 미친듯이 찾아보게 된것이다.
    • 서버를 돌리고 나서 이미지를 업로드하면 서버를 재부팅하지 않는 이상 업로드한 사진을 즉시 서버에 불러올수가 없다. → js나 ajax를 이용해서 실시간으로 왔다갔다 하는 방법도 있는것 같은데, 코드도 복잡해지고 무엇보다 2시간 이상 시도해보았는데도 제대로 동작하지 않았다.
    • 스프링 기본 폴더인 resource/static 폴더를 사용하여 이미지를 업로드 하게 되면, 해당 폴더에 이미지를 업로드 해도 실시간으로 확인되지 않고 서버 재실행 시 확인이 가능하다.
    • 외부 폴더를 추가하여 실제 시스템의 폴더를 사용한 방식은 해당 폴더에 이미지 업로드 시 실시간으로 확인이 가능하다.
  • 두번째 이유 : 이미지 파일을 스프링 부트 resourse폴더에 넣으면 빌드하고 수정하기 힘들다.
  • 세번째 이유 : 내부 폴더(resource)로 설정할 시에는 재배포 할때 파일이 삭제될 가능성이 크다.
    • 배포할때마다 안에 있는 파일이 삭제되는것은 매우 큰 문제이다!!

그리고 또 나같은 경우는 이미지 파일을 저장할때 외부에 저장하는 메서드를 따로 만들었는데, 아래와 같다.

@Service
public class FileStorageService {

    @Value("${file.external-upload-dir}") // application.properties에서 설정한 디렉토리 경로
    private String externalUploadDir;

    public String storeFile(MultipartFile file, String fileName) throws IOException {
        Path uploadPath = Path.of(externalUploadDir).toAbsolutePath().normalize();

        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        String originalFileName = StringUtils.cleanPath(file.getOriginalFilename());
        String fileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));

        String storedFileName = fileName + fileExtension;

        Path storedFilePath = uploadPath.resolve(storedFileName);
        Files.copy(file.getInputStream(), storedFilePath, StandardCopyOption.REPLACE_EXISTING);

        return storedFileName;
    }

    public Path loadFile(String fileName) {
        return Path.of(externalUploadDir).resolve(fileName);
    }
}

여기서 @Value("${file.external-upload-dir}") 이부분의 경로를

file:

external-upload-dir : "/Users/sola/Documents/GitHub/FaceAutoAttendence/data"

이렇게 설정해놓았다. 그래서 이미지가 저 외부경로에 제대로 저장이 되었다.

흠 근데 여기서는 맨 마지막에 /로 안닫아도 되는지 몰겄다… 그래도 일단 지금은 정상적으로 작동하니 더이상 만지지 않겠다!!!

 

출처 : 

[spring] 이미지 업로드 외부경로 설정

[Thymeleaf] 이미지 외부 경로에서 가져오기

 

2. JPA의 hibernate오류!! 

👇🏻 클릭해주세용 👇🏻

더보기

오류 코드는 다음과 같다.
object references an unsaved transient instance - save the transient instance before flushing

 

JPA 연관 관계 테스트 중에 발생했다.

FK 로 사용되는 컬럼값이 없는 상태에서 데이터를 넣으려다 발생한 에러이다.

 

예를 들어, Person (id, name) 이라는 테이블과 House (id, address, person_id) 라는 테이블 관계가 있을 때, Person 데이터를 넣지 않고 House 데이터를 넣으려고 하면 person_id 값이 없어서 에러가 발생한다.

Person person = new Person("Alice"); 
House house = new House("Seoul", person); 
houseRepository.save(house);    // 에러 발생 (person 의 id 값을 모르는데 테이블에 넣으려고 해서)

 

해결 방법:

연관 관계 매핑을 위해 사용하는 @ManyToOne, @OneToMany, @OneToOne 어노테이션에 cascade 옵션을 설정해준다.

casecade는 "영속성 전이"라고 하는 개념인데, 특정 엔티티를 영속화 할 때 관련된 엔티티도 함께 영속화 한다.

저장할 때만 사용하려면 cascade = CascadeType.PERSIST 로 설정해주면 되며, 전체 적용인 CascadeType.ALL 로 설정해도 된다.

 

3. 엔티티가 save 메서드를 사용해도, 제대로 DB에 저장되지 않는 문제 ⭐️⭐️

👇🏻 클릭해주세용 👇🏻

더보기

→ 각각 service클래스에서 @Transactional(readOnly = true) 애노테이션을 썼기 때문…ㅂㄷㅂㄷ

 

@Transactional(readOnly = true) 어노테이션은 해당 메서드가 읽기 전용 트랜잭션으로 실행되도록 설정한다. 이 경우, 메서드가 데이터를 수정하거나 저장하지 않는 경우에 사용

⇒ service부분에 위의 애노테이션을 사용하는 이유는 조회와 같은 부분에서는 db를 건들면 안되기 때문에…

따라서 addCourseWithStudents 메서드가 실제로 데이터를 저장하는 메서드이기때문에, @Transactional(readOnly = true) 어노테이션을 사용하면 안된다!!

@Transactional 어노테이션은 트랜잭션을 관리하고, 메서드가 데이터베이스 작업을 포함하고 있다면 해당 메서드에 어노테이션을 붙여야 한다!

따라서 addCourseWithStudents 메서드에는 @Transactional 어노테이션을 추가로 붙여야한다!

courseService 클래스에 @Transactional(readOnly = true) 어노테이션을 붙인 경우, 이 클래스 내부의 모든 메서드가 읽기 전용 트랜잭션으로 실행되므로, 해당 클래스 내에서 데이터를 수정하거나 저장하는 메서드는 문제가 발생할 수 있다. 따라서 @Transactional 어노테이션을 사용할 때는 메서드에 적합한 속성을 설정하여 적절한 트랜잭션 동작을 보장해야 한다.

 

결론! 다음과 같이 생각하자! 머리에 박아~!!!

@Tranctional 애노테이션 = DB에 저장을 할때!!! 

 

4. 사진을 외부 폴더에 저장하고 싶다!

👇🏻 클릭해주세용 👇🏻

더보기

파일을 저장하는 메서드를 따로 service 클래스로 작성했다.

@Service
public class FileStorageService {

    @Value("${file.external-upload-dir}") // application.properties에서 설정한 디렉토리 경로
    private String externalUploadDir;

    public String storeFile(MultipartFile file, String fileName) throws IOException {
        Path uploadPath = Path.of(externalUploadDir).toAbsolutePath().normalize();

        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        String originalFileName = StringUtils.cleanPath(file.getOriginalFilename());
        String fileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));

        String storedFileName = fileName + fileExtension;

        Path storedFilePath = uploadPath.resolve(storedFileName);
        Files.copy(file.getInputStream(), storedFilePath, StandardCopyOption.REPLACE_EXISTING);

        return storedFileName;
    }

    public Path loadFile(String fileName) {
        return Path.of(externalUploadDir).resolve(fileName);
    }
}

 

그리고 회원가입 컨트롤러 코드에서 다음과 같이 메서드를 호출해서 사진을 저장해주었다.

String profilePicture = null;
        MultipartFile profilePictureFile = form.getProfilePictureFile();
        if (profilePictureFile != null && !profilePictureFile.isEmpty()) {
            profilePicture = fileStorageService.storeFile(profilePictureFile, professor.getName()); //이름으로 저장
            professor.setProfilePicture(profilePicture);
        }

        Long professorId = professorService.join(professor);

 

5. 성적을 입력할때, 어떻게 학생과 성적을 연결해서 저장할것인가?

👇🏻 클릭해주세용 👇🏻

더보기

Map을 이용했다.

이 경우 각 학생의 ID를 키로 사용하고, 그에 해당하는 성적을 값으로 저장하여 관리한다. 이렇게 하면 HTML 폼에서 동적으로 학생들의 성적을 입력하고, 서버에서 쉽게 처리할 수 있다.

맵을 사용하는 이점:
     - 동적인 데이터 관리: 학생 수가 가변적이거나 동적으로 생성되는 경우에 맵을 사용하면 유연하게 데이터를 관리할 수 있다.
     - 간결한 코드: 맵을 사용하면 코드가 간결해지며, 학생 수가 많아질수록 반복 작업을 최소화할 수 있다.

 /**
 * 성적 입력
 */
    @GetMapping("/{professorId}/course/{courseId}/grade")
    public String showGradeForm(@PathVariable Long professorId, @PathVariable Long courseId, Model model) {
        Professor professor = professorService.findOne(professorId); // 교수 정보 가져오기
        Course course = professorService.findCourseById(courseId);

        if (course == null || !course.getProfessor().getId().equals(professorId)) {
            return "redirect:/professor/" + professorId;
        }

        List<Student> students = courseService.findStudentsByCourseId(courseId);
        Map<Long, String> studentGradesMap = new HashMap<>(); //학생별 성적을 담을 Map 초기화

        for (Student student : students) {
            studentGradesMap.put(student.getId(), ""); //학생별 빈 성적 값으로 초기화
        }

        model.addAttribute("professor", professor); // 모델에 교수 객체 추가
        model.addAttribute("course", course);
        model.addAttribute("students", students);
        model.addAttribute("studentGradesMap", studentGradesMap);

        return "professor/grade-form";
    }
    
    @PostMapping("/{professorId}/course/{courseId}/grade")
    public String saveGrades(@PathVariable Long professorId, @PathVariable Long courseId, @RequestParam Map<Long, String> gradeMap) {
        for (Map.Entry<Long, String> entry : gradeMap.entrySet()) {

            Long studentId = Long.parseLong(String.valueOf(entry.getKey())); //키값이 학생ID
            String grade = entry.getValue(); //벨류값이 학생성적

            //점수 저장(서비스 코드에 트랙잭션 필수)
            professorService.gradeStudent(professorId, studentId, courseId, grade);
        }

        return "redirect:/professor/" + professorId + "/course/" + courseId;
    }

ㄴ 컨트롤러 코드

 

<tr th:each="student : ${students}">
    <td><span th:text="${student.name}"></span></td>
    <td><span th:text="${student.major}"></span></td>
    <td><span th:text="${student.sId}"></span></td>
    <td>
        <select th:name="${student.id}">
            <option value="A+">A+</option>
            <option value="A">A</option>
            <option value="B+">B+</option>
            <option value="B">B</option>
            <option value="C+">C+</option>
            <option value="F">F</option>
        </select>
    </td>
</tr>

 ㄴ 다음과 같은 식으로 프론트에서 입력 받으면 됩니당

 

 

📌 AI 모델과 백엔드 연결 과정

- AI model server: Flask
- Web server: Spring boot

 

시스템 구성도

 

⭐️ 수 많은 오류들과 시행착오..👀(흑흑.. 꼭 봐주세요)⬇️

1. 웹캠으로 찍은 사진을 어떻게 AI 모델을 갖는 Flask 서버로 보낼 것 인가!(이게 사실 제일 중요💥)

👇🏻 클릭해주세용 👇🏻

더보기

웹캠을 화면에 구현하고, 웹캠으로 찍은 사진을 파이썬으로 보내는데에 있어서 어려움을 겪었다.


📸 웹캠 화면에 구현하는법

<!-- 웹캠띄우기-->
<script>
    navigator.mediaDevices.getUserMedia({video: true})
        .then(function (stream) {
            var video = document.getElementById('video');
            video.srcObject = stream;
            video.onloadedmetadata = function (e) {
                video.play();
            };
        })
        .catch(function (err) {
            console.log(err);
        });
</script>

 

 

💬 서버로 데이터 전송하고 응답받는 법


사진은 restAPI를 이용하여 flask 서버로 보내는 방식을 채택했다.

 

이것저것 찾아본 결과 방법은 크게 두가지였다.

1. 컨트롤러에서 메서드구현

학생 성적을 입력하는 컨트롤러 코드에서 웹캠 구현파이썬서버로 사진을 보내는 코드를 모두 담당하도록 구성한다.

String input_str = "r"; // 테스트를 위해 'r'을 사용
    if (input_str.equals("r")) {
        // 웹캠 캡처 로직 추가
        byte[] capturedImage = captureWebcamImage();

        // 이미지 전송 및 결과 수신
        JSONObject jsonResponse = sendImageToPythonServer(capturedImage);
        if (jsonResponse != null) {
            System.out.println("Received JSON response: " + jsonResponse);
            String recognizedName = jsonResponse.getString("name");
            String timestamp = jsonResponse.getString("timestamp");

            // Student 객체 조회 (recognizedName으로 학생 정보 조회 필요)
            Student student = studentService.findByName(recognizedName);

            // 출석 기록
            professorService.markStudentAttendance(professorId, student.getId(), courseId, LocalDate.parse(timestamp), request.getStatus());
        } else {
            System.out.println("Failed to receive JSON response");
        }

ㄴ 위 코드는 성적입력 메인 메서드의 코드 중 일부이다.
captureWebcamImage()메서드와 sendImageToPythonServer(byte[] imageBytes) 메서드를 호출하여 성적을 조회하는 로직을 가지고 있다. 

⚽️ captureWebcamImage(): 웹캠을 불러오는 메서드

private byte[] captureWebcamImage() throws IOException {
    // 웹캠 캡처 로직 추가
    Webcam webcam = Webcam.getDefault();
    webcam.open();
    BufferedImage capturedImage = webcam.getImage();

    // 이미지를 바이트 배열로 변환
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ImageIO.write(capturedImage, "jpg", byteArrayOutputStream);
    byte[] imageBytes = byteArrayOutputStream.toByteArray();

    webcam.close();

    return imageBytes;
}

 

⚽️ sendImageToPythonServer(byte[] imageBytes): 캡쳐한 이미지를 Flask 서버로 전송하는 메서드

private JSONObject sendImageToPythonServer(byte[] imageBytes) {
    try {
        String pythonServerUrl = "http://192.168.0.25:5001"; // Python 서버 주소
        HttpPost httpPost = new HttpPost(pythonServerUrl + "/process_attendance");
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 이미지 바이트 배열을 요청 본문에 설정
        ByteArrayEntity entity = new ByteArrayEntity(imageBytes);
        httpPost.setEntity(entity);

        CloseableHttpResponse response = httpClient.execute(httpPost);
        HttpEntity responseEntity = response.getEntity();
        String responseBody = EntityUtils.toString(responseEntity);

        JSONObject jsonResponse = new JSONObject(responseBody);
        return jsonResponse;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

 

 

2. html 클라이언트에서 JavaScript를 이용하여 메서드 구현 ✅

  • Ajax 요청: 클라이언트에서는 Ajax를 사용하여 Flask 서버에 요청을 보내고, 해당 서버에서는 JSON 형식으로 응답하도록 구현
  • 클라이언트에서의 응답 처리: 클라이언트는 Flask 서버로부터 받은 응답을 처리하고, 필요한 데이터를 Spring 서버로 전달하는 역할을 수행
async function captureImage() {
    const videoElement = document.getElementById('video');
    const canvasElement = document.getElementById('canvas');
    const context = canvasElement.getContext('2d');
    context.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);

    const imageDataURL = canvasElement.toDataURL('image/jpeg');

    // 캡쳐한 이미지 화면에 띄우기
    const capturedImageElement = document.getElementById('captured-image');
    capturedImageElement.src = imageDataURL;

    // Python 서버로 캡쳐한 이미지 보내기
    const response = await fetch('http://127.0.0.1:5001/process-attendance', {
        method: 'POST',
        body: JSON.stringify({image: imageDataURL}),
        headers: {
            'Content-Type': 'application/json'
        },
        mode: 'cors' // CORS 모드로 요청
    });

    const responseData = await response.json(); //응답받은 json 객체 저장
    jsonDataList.push(responseData); //리스트에 json 데이터 추가
    console.log(jsonDataList);
    console.log(typeof jsonDataList);
    console.log(typeof JSON.stringify(jsonDataList));

    // 응답데이터를 화면에 띄우기
    const resultElement = document.getElementById('result');
    resultElement.style.display = 'block';
    // 응답데이터 정제해서 띄우기
    const formattedResponse = `${responseData.name}님 출석완료했습니다.\nTimeStamp: ${responseData.timestamp}`;
    const resultDataElement = document.getElementById('result-data');
    resultDataElement.innerText = formattedResponse;
}
코드를 만드는 중간에 테스트 해보았는데 확실히 json형태의 응답데이터를 잘 받아오는것을 확인할 수 있었다.

 

  • Spring 서버에서의 데이터 수신: Spring 서버에서는 클라이언트로부터 받은 데이터를 받아 처리하고, 데이터베이스에 저장하거나 추가 작업을 수행
function prepareFormAndSubmit() {
    const professorId = document.getElementById('professorId').value;
    const courseId = document.getElementById('courseId').value;
    // const attendanceForm = document.getElementById('attendance-form');
    console.log(professorId);
    console.log(courseId);
    fetch('http://127.0.0.1:8000/professor/' + professorId + '/course/' + courseId + '/mark-attendance', {
        method: 'POST',
        body: JSON.stringify(jsonDataList),
        headers: {
            'Content-Type': 'application/json'
        },
        mode: 'cors' // CORS 모드로 요청
    })
        .then(response => response.json())
        .then(data => {
            // 응답 처리
        })
        .catch(error => {
            console.error('Error:', error);
        });
}

 

 

이 기능을 구현하다 보니 깨달은 점 💡


1. 클라이언트에서 서버로 통신하기 위해선 fetch함수를 쓰면 된다...?
method부분에서 통신 방향을 설정해준뒤, body에 전송을 희망하는 메세지를 넣고, headers에 콘텐트 타입을 입력하여 보내려고하는 데이터의 타입을 알려준다.

⭐️⭐️⭐️ 그리고 mode : 'cors' !! 이걸 추가로 붙여줘야한다는 것을 몰라서 3일은 고생했다...하... 
어떻게 찾아냈냐면, 직접 서버들을 돌려보고 웹에서 개발자모드로 network 를 보면, 그 기록에 CORS error이라고 뜬다ㅠ

인터넷에 떠도는 예시를 가져와봤습니다... (출처: 구글)



mode : 'cors' 를 붙여줘야하는 이유는 다음과 같다. (GPT 답변...)


mode: 'cors'를 붙여주는 이유는 웹 애플리케이션에서 다른 도메인으로 HTTP 요청을 보낼 때, 브라우저의 Same-Origin Policy (SOP) 때문에 보안 제한이 걸리기 때문입니다. Same-Origin Policy는 웹 페이지에서 다른 도메인으로의 HTTP 요청을 제한하는 보안 메커니즘 중 하나입니다.

이 정책은 웹 페이지의 스크립트가 다른 도메인의 리소스에 접근하는 것을 방지하여 보안을 유지합니다. 그러나 때로는 웹 애플리케이션에서 다른 도메인의 API 또는 리소스에 접근해야 할 때가 있습니다. 이때 mode: 'cors'를 설정해줌으로써 Cross-Origin HTTP 요청을 허용하고 브라우저가 이를 처리할 수 있도록 도와줍니다.

mode: 'cors'는 Cross-Origin Resource Sharing (CORS)를 활성화하는 옵션입니다. 이 옵션을 사용하면 웹 애플리케이션은 다른 도메인의 리소스에 대한 요청을 보낼 수 있으며, 서버 측에서 적절한 CORS 헤더를 설정하면 요청이 허용됩니다. 이를 통해 다른 도메인 간에 데이터를 공유하거나 API를 사용할 수 있게 됩니다.

따라서 다른 도메인으로의 HTTP 요청을 보내야 하는 경우, mode: 'cors' 옵션을 설정하여 브라우저가 해당 요청을 처리할 수 있도록 해야 합니다.

추가로 이 링크에서 CORS에 관한 더욱 자세한 내용을 알 수 있었다.




그리고 cors 보안문제를 해결하는 다른방법도 찾아보니까 스프링내에서 config를 관리하는 방법도 있었다.

@Configuration
@EnableWebMvc
public class CORSConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 모든 엔드포인트에 대해 CORS 설정을 적용
                .allowedOrigins("*") // 허용할 오리진들 (원하는 도메인을 지정)
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드들
                .allowedHeaders("*") // 허용할 헤더들
                .allowCredentials(true); // 인증 정보 (쿠키 등) 허용 여부
    }
}

이렇게 되면, allow된 methos에서 모든 형식의 콘텐츠 모드의 데이터들의 전송이 cors 에러로부터 자유로워진다.

 

 

 



우리 해커톤의 결과는❤️❤️"광주정보문화산업진흥원 특별상"❤️❤️이었다.

함께 짧지않은 시간동안 힘내준 우리 팀원들에게 너무나 감사하다. 모두 포기하지 않고 끝까지 열심히 각자 맡은바를 잘 해주었다.
다들 정말 능력자였다ㅠㅠ😍

프로젝트 계획을 할때 의도하고자 했던 모든 기능들을 시간안에 구현하는데에는 성공했지만, 프론트를 예쁘게 못꾸민것이 아쉬움이 남는다.. 프론트만 더 예뻤어도 보는이들에게 더큰 즐거움을 줄 수 있었을것 같다. (디자이너분이 정말 예쁘게 디자인해주셨는데ㅠㅠ)
그리고 한계적인 시간때문에 좀더 색다르고 재미있는 기능들을 기획하지 못한것에 대한 아쉬움도 남는다.. 다음엔 해커톤을 위한 단기 프로젝트가 아니라, 장기적으로 제대로 된 프로젝트를 만들어보고싶다!!


그래서 이 해커톤의 결론!!
-> 아직도 스프링의 세계는 어렵다.. 다음번엔 여러가지 예외처리들이나, 여러 타입의 데이터들의 통신 방법 그리고 네트워크 통신에 대한 오류를 잡는 방법도 좀 더 체계적으로 배우고 싶다. 아니 배울것이다.
-> 다음에는 리액트를 공부하고 내가 직접 프론트랑 백엔드를 모두 구현한 프로젝트를 만드는 시간을 가지고 싶다!
-> 개인적인 생각인데, 프론트랑 백엔드 둘다 재미있다... 둘다 엄청 어려운데... 둘다 재미있다... 하지만 둘다 정말 다른 관점으로써의 매력이 있다.. 뭐하나 선택하긴 어려울듯 싶다...

이건 우리 디자이너님이 만드신 "김강정" 캐릭터랍니다!

그럼 모두들 수고 많으셨습니다🎀
다음엔 더 성장한 모습으로 찾아뵐게요!!