카테고리 없음

VDI 시험 감독 대시보드 프론트/백엔드/DB 인수인계 문서

otopligrm 2026. 5. 25. 11:44

1. 이원준 인수인계 by GPT와 함께 만듬

현재까지 작업한 내용은 VDI 시험 감독 대시보드를 실제 DB와 연결하기 위한 프론트엔드/백엔드 API 개발 작업이다.

하지만 실제 운영에서는 학생 로그인 상태, 좌석 상태, 시험 정보, 교수 정보가 모두 DB에 저장되어야 하기 때문에 백엔드 API와 DB를 연결하는 구조로 전환했다.

이번 인수인계의 핵심은 다음이다.

1. 기존 프론트 화면이 어떤 데이터를 필요로 하는지
2. 그 데이터를 백엔드 API가 어떤 형식으로 내려주는지
3. 백엔드 API가 DB의 어떤 테이블을 조회/수정하는지
4. 학생이 VDI에 접속해서 로그인했을 때 좌석 상태가 어떻게 바뀌는지
5. 대시보드가 어떻게 미접속/접속중 상태를 표시하는지
6. 앞으로 DB관리자가 어떤 부분을 관리해야 하는지
 

2. 전체 구조 요약

현재 구조는 다음 흐름으로 동작한다.

[교수 정보 입력 페이지]
        ↓
교수명 / 교번 / 학과 입력
        ↓
백엔드 API 호출
        ↓
ACCOUNT / PROFESSOR_PROFILE 저장 또는 조회

[시험 생성 페이지]
        ↓
과목명 / 시험 유형 / 장소 / 시간 / 정원 입력
        ↓
백엔드 API 호출
        ↓
EXAM_INFO 생성
        ↓
VDI 이름 / IP / 좌석 매핑 테이블 조회
        ↓
VDI_SESSION 초기 생성
        ↓
초기 상태는 OUT, 즉 미접속

[학생 VDI 로그인 페이지]
        ↓
학생이 VDI 접속
        ↓
자동으로 로그인 페이지 실행
        ↓
학번 / 이름 입력
        ↓
백엔드 로그인 API 호출
        ↓
백엔드가 요청 IP 또는 VDI 식별값으로 좌석 확인
        ↓
VDI_SESSION 상태 ACTIVE로 변경

[교수자 대시보드]
        ↓
5초마다 세션 API 호출
        ↓
VDI_SESSION 목록 조회
        ↓
OUT이면 회색
ACTIVE면 초록색
AI_DETECTED면 빨간색
RECONNECTED면 노란색
 

3. 이번 작업의 핵심 개념

3-1. 프론트의 임시 데이터 구조를 DB 기반 구조로 바꿨다

초기 프론트에서는 다음 localStorage 키를 사용했다.

vdi_professor_profile
vdi_exam_info
vdi_sessions
exam_id
 

이 구조는 화면 확인용으로는 충분하지만 실제 시스템에서는 부족하다.

이유는 다음과 같다.

1. 브라우저마다 데이터가 따로 저장된다.
2. 새 PC나 다른 브라우저에서는 데이터를 공유할 수 없다.
3. 학생이 로그인해도 교수자 대시보드에 실시간 반영되지 않는다.
4. 시험 종료 후 기록을 남길 수 없다.
5. 최종 보고서와 연결할 수 없다.
 

그래서 이 구조를 DB 기반으로 바꾸는 것이 이번 작업의 핵심이었다.

vdi_professor_profile  → ACCOUNT / PROFESSOR_PROFILE
vdi_exam_info          → EXAM_INFO
vdi_sessions           → VDI_SESSION
VDI 이름/IP/좌석 정보   → VDI 매핑 테이블
 

4. 프론트엔드 인수인계

4-1. 주요 화면

현재 프론트는 크게 다음 화면으로 나뉜다.

1. 교수 정보 입력 페이지
2. 시험 생성 페이지
3. 학생 로그인 페이지
4. 교수자 대시보드 페이지
 

일반적인 nginx 배포 기준 경로는 다음과 같이 잡았다.

/usr/share/nginx/html/professor/
/usr/share/nginx/html/exam/
/usr/share/nginx/html/login/
/usr/share/nginx/html/home/
 

실제 서버에서 정확한 경로는 다음 명령어로 확인하면 된다.

 
grep -R "root" /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
ls -al /usr/share/nginx/html
 

4-2. 교수 정보 입력 페이지

교수 정보 입력 페이지에서는 다음 값을 받는다.

교수명
교번 또는 교수 식별번호
학과
 

기존 프론트에서는 이 값을 다음 객체로 만들었다.

 
const professorProfile = {
  username: professorNo,
  role: "professor",
  name: professorName,
  department,
  createdAt: new Date().toISOString(),
};
 

기존에는 이것을 localStorage에 저장했다.

 
localStorage.setItem("vdi_professor_profile", JSON.stringify(professorProfile));
 

하지만 실제 운영에서는 백엔드 API로 보내야 한다.

권장 API는 다음과 같다.

 
POST /api/professor/setup
 

요청 예시:

 
{
  "professor_name": "홍길동",
  "professor_no": "P2025001",
  "department": "컴퓨터공학과"
}
 

백엔드에서는 이 값을 DB에 다음처럼 연결한다.

professor_no   → ACCOUNT.username
role           → ACCOUNT.role = professor
professor_name → PROFESSOR_PROFILE.name
department     → PROFESSOR_PROFILE.department
 

DB관리자는 이 부분에서 교수 식별번호가 ACCOUNT.username으로 들어가는 구조를 반드시 기억해야 한다.


4-3. 시험 생성 페이지

시험 생성 페이지에서는 다음 값을 입력받는다.

과목명
시험 장소
정원
시험 시작 시간
시험 종료 시간
시험 유형
 

기존 프론트 객체는 대략 다음 구조였다.

 
const examInfo = {
  exam_id: 1,
  subject_name: subjectName,
  exam_type: examType,
  scheduled_at: startAt,
  ended_at: endAt,
  total_target_students: capacity,
  place: examPlace,
  createdAt: new Date().toISOString(),
};
 

이 값은 DB의 EXAM_INFO 테이블과 연결된다.

subject_name           → EXAM_INFO.subject_name
exam_type              → EXAM_INFO.exam_type
scheduled_at           → EXAM_INFO.scheduled_at
ended_at               → EXAM_INFO.ended_at
total_target_students  → EXAM_INFO.total_target_students
place                  → EXAM_INFO.place
 

권장 API는 다음과 같다.

 
POST /api/exam
 

요청 예시:

 
{
  "professor_username": "P2025001",
  "subject_name": "데이터베이스",
  "exam_type": "중간고사",
  "place": "공학관 301호",
  "scheduled_at": "2026-05-10T13:00:00",
  "ended_at": "2026-05-10T15:00:00",
  "total_target_students": 40
}
 

이 API가 해야 하는 일은 단순히 EXAM_INFO만 만드는 것이 아니다.

1. EXAM_INFO에 시험 정보 저장
2. 생성된 exam_id 확보
3. DB에 있는 VDI 이름/IP/좌석 매핑 조회
4. 정원 수만큼 VDI_SESSION 초기 생성
5. 모든 좌석 상태를 OUT으로 설정
 

즉, 시험 생성 시점에 대시보드에 표시할 좌석 세션도 같이 만들어야 한다.


4-4. 대시보드 페이지

대시보드는 다음 데이터를 필요로 한다.

시험 정보
교수 정보
전체 좌석 수
접속 중 좌석 수
미접속 좌석 수
AI 탐지 좌석 수
재접속 좌석 수
좌석별 상태
학생 학번
학생 이름
VDI 이름
VDI IP
최근 이벤트(보류)
 

대시보드에서 핵심적으로 사용하는 API는 다음 세 개다.

 
GET /api/exam/latest
GET /api/admin/exam?exam_id=1
GET /api/admin/sessions?exam_id=1
 

/api/exam/latest

대시보드에 exam_id가 없을 때 최신 시험을 찾기 위해 사용한다.

예를 들어 사용자가 다음 주소로 들어왔다고 하자.

/home/
 

이 경우 프론트는 어떤 시험을 보여줘야 하는지 모른다.

그래서 먼저 최신 시험을 조회한다.

 
GET /api/exam/latest
 

응답 예시:

 
{
  "success": true,
  "exam": {
    "exam_id": 1,
    "subject_name": "데이터베이스"
  }
}
 

프론트는 이 응답을 받은 뒤 URL을 다음처럼 바꿀 수 있다.

/home/?exam_id=1
 

/api/admin/exam?exam_id=1

대시보드 상단의 시험 정보를 표시하기 위해 사용한다.

응답 예시:

 
{
  "success": true,
  "exam": {
    "exam_id": 1,
    "subject_name": "데이터베이스",
    "exam_type": "중간고사",
    "professor_name": "홍길동",
    "department": "컴퓨터공학과",
    "scheduled_at": "2026-05-10 13:00:00",
    "ended_at": "2026-05-10 15:00:00",
    "place": "공학관 301호"
  }
}
 

이 API는 DB에서 EXAM_INFO만 조회해서는 부족할 수 있다.

교수명과 학과를 표시하려면 다음 테이블과 JOIN이 필요할 수 있다.

EXAM_INFO
ACCOUNT
PROFESSOR_PROFILE
 

DB관리자는 실제 스키마에 맞춰 JOIN 관계를 확인해야 한다.


/api/admin/sessions?exam_id=1

대시보드의 가장 중요한 API다.

이 API 응답을 기준으로 다음 화면이 모두 갱신된다.

KPI 카드
좌석 맵
응시자 현황 테이블
실시간 이벤트 피드
상태 분포
보고서 요약
 

응답 예시:

 
{
  "success": true,
  "sessions": [
    {
      "session_id": 1,
      "seat_no": "A-01",
      "vdi_name": "vdi-01",
      "vdi_ip": "192.168.122.101",
      "student_no": "20231042",
      "student_name": "김OO",
      "state_name": "ACTIVE",
      "started_at": "2026-05-10 13:01:00",
      "last_heartbeat": "2026-05-10 13:05:10",
      "reconnect_count": 0
    },
    {
      "session_id": 2,
      "seat_no": "A-02",
      "vdi_name": "vdi-02",
      "vdi_ip": "192.168.122.102",
      "student_no": null,
      "student_name": null,
      "state_name": "OUT",
      "started_at": null,
      "last_heartbeat": null,
      "reconnect_count": 0
    }
  ]
}
 

대시보드는 이 sessions 배열만 정확히 받으면 대부분의 화면을 자동으로 그릴 수 있다.


5. 대시보드 상태값 규칙

프론트에서 사용하는 상태값은 다음 기준으로 맞춰야 한다.

OUT          → 미접속, 회색
ACTIVE       → 접속중/정상, 초록색
RECONNECTED  → 재접속, 노란색
WARNING      → 주의, 노란색
CAUTION      → 주의, 노란색
AI_DETECTED  → AI 탐지, 빨간색
BLOCKED      → 차단, 빨간색

이거는 GPT랑 만들다보니 너무 많아져서 원준이 너가 대시보드 보고 간추려야 해
 

DB에 저장된 값이 다르다면 백엔드에서 변환해서 내려줘야 한다.

예를 들어 DB에는 숫자 상태값이 있을 수 있다.

1 → OUT
2 → ACTIVE
3 → AI_DETECTED
4 → RECONNECTED
5 → BLOCKED
 

이 경우 API 응답에서는 프론트가 이해할 수 있도록 문자열로 변환하는 것이 좋다.

 
{
  "state_name": "ACTIVE"
}
 

프론트와 백엔드가 상태값을 다르게 쓰면 좌석 색상이 정상적으로 표시되지 않는다.


6. 학생 로그인 페이지 인수인계

학생은 우리가 제공한 VDI 접속 정보로 VDI에 접속한다.

VDI 접속 IP
VDI 접속 포트
VDI 비밀번호
 

VDI에 접속하면 Windows 시작 프로그램 또는 스케줄러를 통해 자동으로 로그인 페이지가 열린다.

http://서버주소/login/
 

학생은 로그인 페이지에서 다음 정보를 입력한다.

학번
이름
 

그리고 백엔드 로그인 API를 호출한다.

현재 논의에서 이름이 조금 혼재되어 있었다.

 
POST /api/vdi/login
 

또는 개념상:

 
POST /api/session/login
 

실제 코드에 이미 /api/vdi/login으로 구현되어 있다면 그걸 유지해도 된다.
다만 인수인계 후에는 DB관리자가 엔드포인트 이름을 하나로 통일하는 것이 좋다.

권장 요청 예시:

 
{
  "student_no": "20231042",
  "student_name": "김OO"
}
 

응답 예시:

 
{
  "success": true,
  "message": "로그인되었습니다.",
  "session": {
    "session_id": 1,
    "seat_no": "A-01",
    "vdi_name": "vdi-01",
    "vdi_ip": "192.168.122.101",
    "student_no": "20231042",
    "student_name": "김OO",
    "state_name": "ACTIVE"
  }
}
 

7. 학생마다 로그인 URL을 다르게 만들 필요가 없는 이유

중요한 인수인계 포인트다.

현재 DB에는 이미 다음 매핑이 있다고 했다.

VDI 이름 ↔ VDI IP ↔ 좌석 이름
 

예를 들면 다음과 같다.

vdi-01 ↔ 192.168.122.101 ↔ A-01
vdi-02 ↔ 192.168.122.102 ↔ A-02
vdi-03 ↔ 192.168.122.103 ↔ A-03
 

따라서 학생마다 로그인 브라우저 주소를 다르게 만들 필요는 없다.

모든 VDI에서 같은 로그인 페이지를 열어도 된다.

http://서버주소/login/
 

대신 백엔드가 로그인 요청을 받았을 때 다음을 해야 한다.

1. 요청이 들어온 클라이언트 IP 확인
2. DB의 VDI 이름/IP/좌석 매핑 테이블 조회
3. 해당 IP가 어느 VDI인지 확인
4. 해당 VDI가 어느 좌석인지 확인
5. 해당 좌석의 VDI_SESSION 업데이트
 

즉, 핵심은 URL이 아니라 백엔드가 VDI를 식별할 수 있느냐다.


8. 주의할 점: nginx 프록시를 쓰면 실제 VDI IP가 안 보일 수 있음

이 부분은 DB관리자에게 반드시 전달해야 한다.

학생 브라우저가 직접 FastAPI 서버로 요청하면 FastAPI에서 request.client.host로 VDI IP를 볼 수 있다.

하지만 보통 운영에서는 다음 구조를 쓴다.

학생 VDI 브라우저
  ↓
nginx
  ↓
FastAPI / uvicorn
 

이 경우 FastAPI에서 그냥 request.client.host를 보면 실제 VDI IP가 아니라 127.0.0.1 또는 nginx IP로 보일 수 있다.

그러면 백엔드는 어떤 VDI에서 로그인했는지 알 수 없다.

이 문제를 해결하려면 nginx에서 실제 클라이언트 IP를 헤더로 넘겨야 한다.

nginx 설정 예시는 다음과 같다.

 
location /api/ {
    proxy_pass http://127.0.0.1:8000/api/;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}
 

FastAPI에서는 다음 순서로 IP를 확인하는 것이 좋다.

 
def get_client_ip(request):
    x_real_ip = request.headers.get("x-real-ip")
    if x_real_ip:
        return x_real_ip.strip()

    x_forwarded_for = request.headers.get("x-forwarded-for")
    if x_forwarded_for:
        return x_forwarded_for.split(",")[0].strip()

    return request.client.host
 

단, X-Forwarded-For는 외부에서 조작될 수 있으므로, 운영 환경에서는 nginx가 신뢰할 수 있는 내부 프록시일 때만 사용해야 한다.


9. 백엔드 인수인계

9-1. 백엔드 기술 스택

현재 백엔드는 FastAPI 기반으로 구성한 것으로 정리하면 된다.

사용 패키지는 다음과 같다.

fastapi
uvicorn
pymysql
python-dotenv
 

설치 예시:

 
cd /opt/vdi-login-api

python3 -m venv venv
source venv/bin/activate

pip install fastapi uvicorn pymysql python-dotenv
 

9-2. 백엔드 환경변수

DB 접속 정보는 코드에 직접 쓰지 말고 .env에 관리한다.

예시:

 
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=DB_USER_NAME
DB_PASSWORD=DB_PASSWORD
DB_NAME=capston_db

STATE_OUT_ID=1
STATE_ACTIVE_ID=2
STATE_AI_DETECTED_ID=3
STATE_RECONNECTED_ID=4
STATE_BLOCKED_ID=5
 

인수인계 시 실제 비밀번호는 공개 문서에 넣으면 안 된다.

주의:
DB 계정
DB 비밀번호
서버 공인 IP
SSH 계정
VDI 접속 비밀번호
실제 학생 학번/이름
 

위 정보는 블로그나 공개 문서에 올리면 안 된다.


9-3. systemd 서비스

이전 작업에서 FastAPI를 systemd 서비스로 등록했다.

중요한 점은 이것이다.

FastAPI는 사용자가 SSH 접속할 때마다 새로 실행되는 것이 아니다.
systemd 서비스로 등록되어 있으면 서버 부팅 시 한 번 실행된다.
여러 명이 서버에 SSH로 접속해도 FastAPI가 여러 번 자동 실행되지는 않는다.
 

서비스 파일 위치는 보통 다음과 같다.

 
/etc/systemd/system/vdi-login-api.service
 

상태 확인:

 
sudo systemctl status vdi-login-api
 

서비스 시작:

 
sudo systemctl start vdi-login-api
 

서비스 재시작:

 
sudo systemctl restart vdi-login-api
 

서버 부팅 시 자동 실행:

 
sudo systemctl enable vdi-login-api
 

로그 확인:

 
sudo journalctl -u vdi-login-api -f
 

최근 로그 확인:

 
sudo journalctl -u vdi-login-api -n 100
 

포트 확인:

 
ss -ltnp | grep 8000
 

직접 uvicorn을 여러 터미널에서 실행하면 포트 충돌이 날 수 있다.

주의:
개발자가 수동으로 uvicorn main:app --host 0.0.0.0 --port 8000 을 실행한 상태에서
systemd 서비스도 실행하면 8000 포트 충돌이 발생할 수 있다.
 

10. 백엔드 API 목록

DB관리자가 앞으로 관리해야 할 주요 API는 다음과 같다.

기능메서드API설명
상태 확인 GET /api/health 백엔드 서버 정상 여부 확인
교수 정보 저장 POST /api/professor/setup 교수명, 교번, 학과 저장
시험 생성 POST /api/exam 시험 정보 저장 및 세션 생성
최신 시험 조회 GET /api/exam/latest 대시보드 기본 exam_id 조회
시험 상세 조회 GET /api/admin/exam?exam_id=... 대시보드 상단 시험 정보
세션 목록 조회 GET /api/admin/sessions?exam_id=... 좌석 상태 목록
학생 로그인 POST /api/vdi/login 또는 /api/session/login 학생 로그인 및 좌석 ACTIVE 처리
접속 유지 POST/PATCH /api/session/heartbeat 향후 접속 유지 판단용
탐지 반영 POST /api/detection 향후 AI 탐지/차단 상태 반영용

현재 대시보드가 반드시 필요로 하는 핵심 API는 다음 세 개다.

GET /api/exam/latest
GET /api/admin/exam?exam_id=...
GET /api/admin/sessions?exam_id=...
 

학생 로그인까지 정상적으로 연결하려면 다음 API도 필수다.

POST /api/vdi/login
 

11. DB 테이블 역할 인수인계

정확한 테이블명은 실제 DB 스키마를 기준으로 다시 확인해야 하지만, 우리가 개발하면서 기준으로 잡은 역할은 다음과 같다.

ACCOUNT

사용자 계정 정보를 관리한다.

교수 계정
학생 계정
관리자 계정
role 구분
username
password 또는 인증 정보
 

교수 정보 입력 화면의 professorNo는 ACCOUNT.username으로 연결할 수 있다.


PROFESSOR_PROFILE

교수 상세 정보를 관리한다.

교수명
학과
ACCOUNT와의 연결 키
 

대시보드 상단에서 교수명과 학과를 보여줄 때 필요하다.


EXAM_INFO

시험 정보를 관리한다.

exam_id
subject_name
exam_type
place
scheduled_at
ended_at
total_target_students
professor_id 또는 account_id
created_at
 

시험 생성 페이지에서 입력한 정보가 이 테이블에 저장된다.


VDI 이름/IP/좌석 매핑 테이블

현재 DB에 이미 존재하는 중요한 테이블이다.

역할은 다음과 같다.

VDI 이름
VDI IP
좌석 이름 또는 좌석 번호
 

예시:

vdi-01 / 192.168.122.101 / A-01
vdi-02 / 192.168.122.102 / A-02
vdi-03 / 192.168.122.103 / A-03
 

이 테이블 덕분에 학생마다 로그인 URL을 다르게 만들 필요가 없다.

학생이 로그인하면 백엔드가 요청 IP를 확인하고 이 테이블에서 좌석을 찾는다.


VDI_SESSION

시험별 좌석 세션을 관리한다.

session_id
exam_id
seat_no
vdi_name
vdi_ip
student_no
student_name
current_state
started_at
last_heartbeat
reconnect_count
 

초기 상태:

student_no = null
student_name = null
current_state = OUT
 

학생 로그인 후:

student_no = 입력한 학번
student_name = 입력한 이름
current_state = ACTIVE
started_at = 현재 시간
last_heartbeat = 현재 시간
 

대시보드는 이 테이블을 조회해서 좌석 색상을 결정한다.


SESSION_STATE

상태 코드 관리 테이블로 사용할 수 있다.

예시:

1 / OUT / 미접속
2 / ACTIVE / 접속중
3 / AI_DETECTED / AI 탐지
4 / RECONNECTED / 재접속
5 / BLOCKED / 차단
 

이전에 한글 설명을 insert할 때 MariaDB 문자셋 문제로 오류가 날 수 있었다.

오류 예시:

ERROR 1366 Incorrect string value
 

해결 방향:

 
ALTER DATABASE capston_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
 

테이블과 컬럼도 utf8mb4인지 확인해야 한다.

 
SHOW FULL COLUMNS FROM SESSION_STATE;
 

12. 학생 로그인 처리 로직

학생 로그인 API의 핵심 로직은 다음이다.

1. 학생 학번과 이름을 body로 받는다.
2. 요청 IP를 확인한다.
3. 요청 IP로 VDI 매핑 테이블 조회
4. VDI 이름과 좌석 번호 확인
5. 현재 진행 중인 exam_id 확인
6. 해당 exam_id + seat_no의 VDI_SESSION 조회
7. student_no, student_name 저장
8. current_state를 ACTIVE로 변경
9. started_at, last_heartbeat 갱신
10. 프론트에 success true 반환
 

개념 SQL:

 
SELECT
  vdi_name,
  vdi_ip,
  seat_no
FROM VDI_SEAT_MAPPING
WHERE vdi_ip = ?;
 

세션 업데이트:

 
UPDATE VDI_SESSION
SET
  student_no = ?,
  student_name = ?,
  current_state = 'ACTIVE',
  started_at = COALESCE(started_at, NOW()),
  last_heartbeat = NOW()
WHERE
  exam_id = ?
  AND seat_no = ?;
 

DB에 상태를 숫자로 저장한다면 다음처럼 처리할 수 있다.

 
UPDATE VDI_SESSION
SET
  student_no = ?,
  student_name = ?,
  state_id = 2,
  started_at = COALESCE(started_at, NOW()),
  last_heartbeat = NOW()
WHERE
  exam_id = ?
  AND seat_no = ?;
 

그리고 API 응답에서는 숫자 대신 문자열로 변환해서 내려준다.

 
{
  "state_name": "ACTIVE"
}
 

13. 시험 생성 시 세션 생성 로직

시험 생성 API는 다음 순서로 동작해야 한다.

1. 교수 계정 확인
2. EXAM_INFO insert
3. 생성된 exam_id 확인
4. VDI 매핑 테이블에서 정원 수만큼 좌석 조회
5. VDI_SESSION에 초기 세션 insert
6. 모든 상태는 OUT
 

개념 SQL:

 
INSERT INTO EXAM_INFO (
  subject_name,
  exam_type,
  place,
  scheduled_at,
  ended_at,
  total_target_students,
  professor_id
)
VALUES (?, ?, ?, ?, ?, ?, ?);
 

VDI 매핑 조회:

 
SELECT
  vdi_name,
  vdi_ip,
  seat_no
FROM VDI_SEAT_MAPPING
ORDER BY seat_no
LIMIT ?;
 

세션 초기 생성:

 
INSERT INTO VDI_SESSION (
  exam_id,
  seat_no,
  vdi_name,
  vdi_ip,
  student_no,
  student_name,
  current_state,
  started_at,
  last_heartbeat,
  reconnect_count
)
VALUES (
  ?,
  ?,
  ?,
  ?,
  NULL,
  NULL,
  'OUT',
  NULL,
  NULL,
  0
);
 

이렇게 해야 대시보드에 처음 들어갔을 때 전체 좌석이 회색으로 보인다.


14. 대시보드 갱신 구조

대시보드는 일정 주기마다 API를 다시 호출한다.

현재 기준은 5초 polling 구조다.

 
loadDashboard();
setInterval(loadDashboard, 5000);
 

흐름은 다음과 같다.

학생 로그인
  ↓
POST /api/vdi/login
  ↓
DB에서 해당 좌석 ACTIVE 변경
  ↓
대시보드가 5초 후 GET /api/admin/sessions 호출
  ↓
새로운 sessions 배열 수신
  ↓
해당 좌석 회색 → 초록색
 

WebSocket을 쓰지 않아도 초기 프로젝트 단계에서는 충분히 동작한다.

향후 실시간성을 높이고 싶다면 WebSocket 또는 SSE로 변경할 수 있다.


15. API 응답 규칙

프론트의 fetchJson 함수는 응답에 success가 있는지 확인한다.

따라서 모든 API 응답은 다음 형식을 지켜야 한다.

성공:

 
{
  "success": true,
  "data": {}
}
 

또는:

 
{
  "success": true,
  "exam": {},
  "sessions": []
}
 

실패:

 
{
  "success": false,
  "message": "오류 메시지"
}
 

주의할 점은 HTTP 200이어도 success가 없으면 프론트에서 실패로 처리될 수 있다는 것이다.

나쁜 예:

 
{
  "exam": {
    "exam_id": 1
  }
}
 

좋은 예:

 
{
  "success": true,
  "exam": {
    "exam_id": 1
  }
}
 

16. 운영 명령어 인수인계

백엔드 상태 확인

 
sudo systemctl status vdi-login-api
 

백엔드 재시작

 
sudo systemctl restart vdi-login-api
 

백엔드 로그 확인

 
sudo journalctl -u vdi-login-api -f
 

FastAPI 포트 확인

 
ss -ltnp | grep 8000
 

nginx 상태 확인

 
sudo systemctl status nginx
 

nginx 재시작

 
sudo systemctl restart nginx
 

nginx 설정 테스트

 
sudo nginx -t
 

80번 포트 확인

 
ss -ltnp | grep ':80'
 

API health check

 
curl http://127.0.0.1:8000/api/health
 

nginx를 통해 확인:

 
curl http://서버주소/api/health
 

17. API 테스트 명령어

최신 시험 조회

 
curl http://서버주소/api/exam/latest
 

시험 상세 조회

 
curl "http://서버주소/api/admin/exam?exam_id=1"
 

세션 목록 조회

 
curl "http://서버주소/api/admin/sessions?exam_id=1"
 

학생 로그인 테스트

 
curl -X POST "http://서버주소/api/vdi/login" \
  -H "Content-Type: application/json" \
  -d '{
    "student_no": "20231042",
    "student_name": "김OO"
  }'
 

주의할 점은 이 테스트를 서버 내부에서 127.0.0.1로 실행하면 요청 IP가 실제 VDI IP가 아니기 때문에 좌석 매핑이 실패할 수 있다는 것이다.

VDI IP 기반 매핑을 테스트하려면 실제 VDI 안에서 로그인 요청을 보내는 방식으로 확인해야 한다.


18. DB 확인용 SQL

시험 목록 확인

 
SELECT *
FROM EXAM_INFO
ORDER BY exam_id DESC;
 

특정 시험 확인

 
SELECT *
FROM EXAM_INFO
WHERE exam_id = 1;
 

VDI/좌석 매핑 확인

 
SELECT *
FROM VDI_SEAT_MAPPING
ORDER BY seat_no;
 

실제 테이블명이 다르면 현재 DB의 매핑 테이블명으로 바꿔야 한다.

특정 IP가 어느 좌석인지 확인

 
SELECT *
FROM VDI_SEAT_MAPPING
WHERE vdi_ip = '192.168.122.101';
 

특정 시험의 세션 확인

 
SELECT *
FROM VDI_SESSION
WHERE exam_id = 1
ORDER BY seat_no;
 

접속 중 좌석 확인

 
SELECT
  seat_no,
  vdi_name,
  vdi_ip,
  student_no,
  student_name,
  current_state,
  started_at,
  last_heartbeat
FROM VDI_SESSION
WHERE exam_id = 1
  AND current_state = 'ACTIVE';
 

미접속 좌석 확인

 
SELECT
  seat_no,
  vdi_name,
  vdi_ip,
  current_state
FROM VDI_SESSION
WHERE exam_id = 1
  AND current_state = 'OUT';
 

19. 주요 시행착오와 해결 방식

19-1. localStorage 기반이라 실제 DB와 연결되지 않음

문제

초기 프론트는 localStorage만 사용했다.

vdi_professor_profile
vdi_exam_info
vdi_sessions
 

원인

프론트 화면 확인을 위해 임시 데이터를 사용한 구조였기 때문이다.

해결

백엔드 API를 만들고 DB와 연결했다.

교수 정보 → ACCOUNT / PROFESSOR_PROFILE
시험 정보 → EXAM_INFO
좌석 상태 → VDI_SESSION
 

19-2. exam_id가 없으면 대시보드가 어떤 시험을 보여줄지 모름

문제

사용자가 /home/으로 접속하면 exam_id가 없다.

해결

GET /api/exam/latest API를 만들었다.

/home/
  ↓
/api/exam/latest 호출
  ↓
최신 exam_id 확인
  ↓
/home/?exam_id=1 로 URL 갱신
 

19-3. API 응답에 success가 없어서 프론트에서 오류 처리됨

문제

백엔드가 데이터를 내려줘도 프론트에서 실패로 처리될 수 있었다.

원인

프론트 fetchJson이 data.success를 검사했기 때문이다.

해결

모든 API 응답에 success를 포함한다.

 
{
  "success": true
}
 

19-4. DB 상태값과 프론트 상태값이 다름

문제

DB에는 숫자나 다른 문자열로 상태가 저장되어 있는데, 프론트는 ACTIVE, OUT, AI_DETECTED 등을 기대한다.

해결

백엔드에서 응답 변환을 한다.

1 → OUT
2 → ACTIVE
3 → AI_DETECTED
4 → RECONNECTED
5 → BLOCKED
 

19-5. 학생 로그인 후 어떤 좌석인지 알 수 없음

문제

학생이 학번과 이름만 입력하면 백엔드가 좌석을 알 수 없다.

해결

DB에 이미 있는 VDI 이름/IP/좌석 매핑을 사용한다.

요청 IP
  ↓
VDI 매핑 테이블 조회
  ↓
좌석 확인
  ↓
VDI_SESSION 업데이트
 

19-6. nginx 프록시 때문에 실제 VDI IP가 안 보일 수 있음

문제

FastAPI에서 클라이언트 IP를 확인하면 127.0.0.1로 나올 수 있다.

해결

nginx에서 다음 헤더를 넘긴다.

 
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 

FastAPI에서 해당 헤더를 우선 확인한다.


19-7. 한글 insert 오류

문제

MariaDB에 한글 설명을 넣을 때 다음 오류가 날 수 있었다.

ERROR 1366 Incorrect string value
 

원인

DB, 테이블, 컬럼 문자셋이 utf8mb4가 아니었을 가능성이 있다.

해결

 
ALTER DATABASE capston_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
 

테이블과 컬럼 문자셋도 확인해야 한다.