LS이수종
← 프로젝트 목록

산학연계 SW프로젝트 · SW중심대학사업단 · 마이크로스톤

마모키 출석체크 어플

수기·메신저 출석의 대리 출석 문제를 없애기 위해, BLE 비콘 근접 감지와 현장에서만 알 수 있는 4자리 비밀번호를 동시에 요구합니다. 출석 인정 여부·지각 판정·미출석자 일괄 처리는 전부 서버가 결정합니다.

기간2026.04.01 – 08.30
개발 4 · 디자인 2 · 지도교수 1 · 기업 1
내 역할백엔드 2인 중 1인 · 배포 파이프라인
스택Java 21 · Spring Boot 4 · MySQL · Docker
이 프로젝트는

제가 속한 개발 동아리 마모키에서, 동아리원들이 실제로 쓰려고 만든 출석체크 앱입니다. 매주 모임마다 운영진이 이름을 부르고 수기로 적던 출석을 앱이 대신하게 하는 것이 목표였습니다.

사용자가 곧 제 팀원들이다 보니 “대리 출석을 어떻게 막을 것인가”가 바로 현실적인 문제로 돌아왔습니다. 비콘만 쓰면 복도에서도 출석이 되고 비밀번호만 쓰면 원격으로 전달받아 찍을 수 있어서, 결국 두 조건을 모두 요구하는 구조로 갔습니다. 쓰는 사람이 가까이 있으니 문제 제보가 그날 바로 들어왔고, 그 피드백을 반영해 고치는 주기가 가장 짧았던 프로젝트입니다.

담당 범위

시스템 아키텍처

ESP32-S3 고정 비콘 + 출석 비콘 60초 후 자동 소등 BLE 광고 34B 전문 · PSK Flutter 앱 RSSI 3초 안정화 4자리 비밀번호 입력 React 대시보드 통계 · CSV 내보내기 REST Spring Boot 4 · Java 21 출석 검증 4단계 멤버 → 진행세션 → 비밀번호 → 중복 지각 자동 판정 · 일괄 결석 처리 도메인 8 + global (security · fcm) SSE 실시간 출석 피드 MySQL (회원,세션) 복합 유니크 FCM 세션 시작 · 출석 완료 비콘은 서버와 직접 통신하지 않는다 — 앱이 유일한 다리
비콘에 네트워크·인증 정보를 두지 않아 설치와 교체가 단순해집니다. 대신 명령 위조를 막기 위해 앱→비콘 구간에 고정 길이 전문과 사전 공유 키를 두었습니다.

API 설계 구조

출석은 “어느 동아리의, 어느 세션의” 출석인지가 항상 함께 필요하므로 경로에 소속 관계를 그대로 담았습니다. 실시간 피드만 별도 스트림 경로로 분리했습니다.

경로접근 주체역할
/api/v1/auth/loginPOST학번·비밀번호 인증 → access · refresh 토큰 발급
/api/v1/clubs/{clubId}/sessions/{sessionId}/attendancePOST · GET출석 제출 및 조회 — 서버 4단계 검증
/api/v1/clubs/{clubId}/attendanceGET (SSE)진행 중 세션의 실시간 출석 이벤트 스트림

응답은 RsData<T> 공통 래퍼로 통일하고, springdoc이 코드에서 통신 규격 문서를 자동 생성합니다. 웹 대시보드는 그 문서로부터 타입을 생성하므로, 서버와 클라이언트의 규약이 어긋나면 빌드 단계에서 바로 드러납니다.

트러블슈팅

TS 1중복 출석을 애플리케이션 검사만으로 막을 수 없던 문제
문제

통신 지연으로 같은 멤버의 출석 요청이 시차를 두고 두 번 도달하면, 서비스 코드의 “이미 출석했는지” 검사만으로는 두 요청이 검사 구간을 나란히 통과해 출석 기록이 두 건 생성될 수 있었습니다.

해결
  • 출석 테이블에 (회원, 세션) 복합 유니크 제약을 걸어 DB 차원에서 중복을 차단 — 애플리케이션 검사를 통과하더라도 두 번째 삽입은 제약 위반으로 실패
  • 세션 종료 시 미출석자를 결석 처리하는 작업은 하나의 트랜잭션으로 묶어, 도중에 실패해도 일부만 결석 처리된 상태가 남지 않도록 처리
결과

검사와 기록 사이의 시간 간격이 아무리 짧아도 중복 기록이 남지 않게 됐습니다.

TS 2로그인에 묶여 있던 FCM 토큰 갱신을 분리
문제

처음에는 로그인 요청 본문에 fcmToken을 실어 저장했습니다. 그런데 FCM 토큰은 앱 재설치·기기 변경·만료로 로그인과 무관한 시점에 바뀝니다. 그때마다 재로그인을 해야만 갱신되는 구조였습니다.

해결
  • 로그인 요청에서 fcmToken을 제거하고, 토큰 갱신 전용 API를 만들어 앱이 변경을 감지한 시점에 바로 호출하도록 변경
  • API 문서까지 함께 갱신해 앱 담당자가 변경된 흐름을 문서에서 바로 확인할 수 있게 함
결과

기기를 바꿔도 재로그인 없이 알림이 정상 수신되고, 로그인 API의 책임도 인증 하나로 정리됐습니다.

TS 3출석 인증코드의 수명과 지각 기준을 서버에서 결정하기
문제

4자리 인증코드가 세션 내내 유효하면, 참석하지 않은 사람에게 코드를 전달해 대리 출석하는 것을 막을 수 없습니다. 지각 여부를 관리자가 눈으로 판단하는 것도 기록마다 기준이 달라집니다.

해결
  • 출석 시작 후 일정 시간이 지나면 인증코드를 서버가 자동으로 무효화하고, 지각은 세션 시작 시각 기준 경과 시간으로 서버가 판정하도록 통일
  • 비콘 근접(식별자 + 신호 세기 + 안정화 시간)과 비밀번호를 둘 다 요구해, 어느 한쪽만으로는 출석이 성립하지 않도록 구성
  • 세션 생성 함수를 단일/반복 생성으로 분기해 정기 모임을 한 번에 등록할 수 있도록 확장
결과

“그 장소에 있었을 것”과 “그 시각에 현장에서 안내를 받았을 것”을 동시에 요구하는 구조가 되어, 위치나 비밀번호 한쪽만 확보한 우회 경로가 사라졌습니다.

배포 파이프라인

git tag v* push GitHub Actions JDK 21 · gradlew build Firebase 키 복원 Buildx 멀티스테이지 Docker Hub beacon:{tag} beacon:latest jre-alpine 런타임 서버 (SSH) scp · deploy.sh 동기화 compose pull compose up -d image prune -f 운영 app + MySQL 컨테이너 2종 모든 자격 증명은 GitHub Secrets · 서버의 .env로만 주입 — 저장소에 남기지 않음 Docker Hub 태그가 릴리스 이력이 되므로, 문제가 생기면 이전 태그로 되돌릴 수 있음
배포 트리거를 브랜치 푸시가 아니라 v* 태그로 둔 이유는, 개발 중 변경이 그대로 운영에 나가지 않게 하고 어떤 이미지가 배포됐는지를 태그로 되짚기 위해서입니다.

서비스 화면

앱 홈 — 비콘 감지 상태를 동심원으로 표시
앱 홈 — 비콘 감지 상태를 동심원으로 표시
4자리 출석 비밀번호 입력
4자리 출석 비밀번호 입력
웹 대시보드 — 출석률 추이와 상태 분포
웹 대시보드 — 출석률 추이와 상태 분포
세션 캘린더
세션 캘린더

앱은 Flutter, 웹 대시보드는 React로 팀원들이 구현했습니다. 저는 이 화면들이 호출하는 출석 검증·지각 판정·통계 집계·푸시 발송 API를 담당했습니다.

저장소

Backend ↗Flutter App ↗Web Dashboard ↗ESP32 Firmware ↗
이전← 엄마약다음냉장터 →