Soonlife DOCS
아키텍처

아키텍처

SCH Web Portal 전체 구조, 인증 흐름, 보안 설계, DB 스키마

전체 구조

사용자 브라우저React SPA (Vite)localhost:5173/api/* proxyDjango REST APIlocalhost:8000SCH 통합정보시스템(학교 서버)학적 · 성적 · 시간표 등 실시간 조회MySQL DB즐겨찾기 · 시간표 테마학사일정 · 공지사항

학적·성적·시간표 등 대부분의 데이터는 DB에 저장되지 않고 매 요청마다 SCH 서버에서 실시간 조회합니다.
DB에 저장하는 데이터는 즐겨찾기, 시간표 테마, 학사일정, 공지사항 4종뿐입니다.


백엔드 구조

디렉터리

backend/
├── portal/
│   ├── settings.py    # 전체 설정
│   └── urls.py        # path('api/', include('main.urls'))
└── main/
    ├── models.py       # DB 모델 4종
    ├── urls.py         # 70+ API 경로
    ├── session_crypto.py  # AES-256-GCM 세션 암호화
    ├── views/          # 기능별 뷰 모듈 (40+ 파일)
    │   ├── _common.py  # 세션 쿠키 헬퍼, 공통 상수
    │   ├── auth.py
    │   ├── academic.py
    │   └── ...
    └── sch_client/     # SCH 서버 HTTP 통신 클라이언트
        ├── _common.py  # 공통 요청 로직
        └── ...

요청 처리 흐름

브라우저 요청 (쿠키 포함)


Django View

       ├─ _get_session(request)
       │     ├─ 쿠키에서 sch_session 추출
       │     ├─ session_crypto.decrypt(token, ip, ua)
       │     └─ 만료 시간 확인

       ├─ sch_client.*(std_no, ecd_key, jsessionid, ...)
       │     └─ SCH 서버 HTTP 요청

       └─ Response(data)

세션 암호화 상세

알고리즘

항목
암호화AES-256-GCM
키 파생HKDF-SHA256
HKDF salt"sch-portal-v1"
HKDF info"session-v1"
Nonce12 bytes (랜덤)
GCM Tag16 bytes
AADHMAC-SHA256("ctx-binding", ip + "|" + user_agent)

토큰 구조 (바이너리, Base64URL 인코딩)

┌──────────┬──────────┬───────────────┬────────────────────────────┐
│ version  │  kid     │    nonce      │   ciphertext + GCM tag     │
│ (1 byte) │ (1 byte) │  (12 bytes)   │   (N + 16 bytes)           │
└──────────┴──────────┴───────────────┴────────────────────────────┘

현재 버전: 0x01, 키 ID: 1 (향후 키 교체 시 kid로 구분)

IP + User-Agent 바인딩

AAD(Additional Authenticated Data)에 클라이언트 IP와 User-Agent가 포함됩니다.
따라서 같은 토큰을 다른 IP나 다른 브라우저에서 재사용하면 GCM 인증이 실패합니다.

이 설계의 장점:

  • 쿠키 탈취 후 다른 IP에서 사용 불가
  • 같은 네트워크(사내 NAT)에서도 User-Agent가 다르면 실패

단점:

  • 모바일 로밍 중 IP가 바뀌면 세션이 끊김
  • VPN 환경에서 간헐적으로 IP가 바뀌면 로그아웃됨

세션 페이로드

{
  "std_no":     "20201234",    # 학번
  "user_no":    "U000001234",  # SCH 사용자 번호
  "name":       "홍길동",       # 이름
  "jsessionid": "ABC123...",   # SCH 서버 세션 ID
  "ecd_key":    "ENCKEY...",   # SCH 암호화 키
  "dept_cd":    "CS",          # 학과 코드
  "use_div":    "",            # 재학구분 (항상 빈 문자열)
  "aug_div":    "",            # 인증구분 (항상 빈 문자열)
  "expired":    1700007200     # 만료 Unix timestamp
}

DB 스키마

student_favorites

컬럼타입설명
std_noVARCHAR(20) PK학번
itemsJSON즐겨찾기 항목 이름 배열
updated_atDATETIME최종 수정 시각

timetable_themes

컬럼타입설명
idINT PK자동증가
nameVARCHAR(40)테마 이름
descriptionVARCHAR(140)설명
colorsJSON배경색 배열 (HEX, 3~16개)
text_colorsJSON텍스트색 배열
author_std_noVARCHAR(20) INDEX작성자 학번
is_publicBOOLEAN공개 여부
use_countINT UNSIGNED적용 횟수
created_atDATETIME생성 시각
updated_atDATETIME수정 시각

정렬 기본값: (-use_count, -created_at) — 많이 사용된 테마가 상위에 표시

student_timetable_themes

컬럼타입설명
std_noVARCHAR(20) PK학번
theme_idINT FK (nullable)적용 테마 ID. NULL이면 기본값
updated_atDATETIME최종 수정 시각

테마 적용 시 timetable_themes.use_count를 원자적으로 +1 증가시킵니다.

academic_events

컬럼타입설명
idINT PK자동증가
start_dateDATE일정 시작일
end_dateDATE일정 종료일
contentVARCHAR(255)일정 내용

정렬 기본값: start_date 오름차순

chat_conversations

AI 챗봇 대화 기록 (클라우드 동기화에 동의한 사용자만 해당 — 학번 기준으로 저장).

컬럼타입설명
idINT PK자동증가
std_noVARCHAR(20) INDEX학번
titleVARCHAR(100)대화 제목 — 첫 질문에서 자동 생성
turnsJSON[{question, answer, trace}]
last_noticeJSON (nullable)[board, id] — 후속 질문 문맥용
import_keyVARCHAR(80) (nullable)로컬→서버 이전 시 멱등 처리 키
created_at / updated_atDATETIME생성/수정 시각

UniqueConstraint(std_no, import_key), 정렬 기본값: -updated_at

chat_logs

챗봇 전체 문답의 익명 로그 — 답변 품질 검토용. 학번 등 식별 정보는 저장하지 않는다.

컬럼타입설명
idINT PK자동증가
thread_keyVARCHAR(36) INDEX대화 식별자를 비밀키(HMAC)로 해시한 값 — 같은 대화의 턴을 묶되 원래 대화·사용자로 역추적 불가
question / answerTEXT질문/답변 원문
traceJSON진행 과정 문구 (디버깅·검토용)
high_qualityBOOLEAN고성능 모드 답변 여부
created_atDATETIME생성 시각

정렬 기본값: -created_at. 저장 설정(로컬/클라우드)과 무관하게 항상 기록된다.

chat_feedback

답변 평가(좋아요/싫어요) — 추후 RLHF 학습 데이터로 활용 예정. 익명 수집.

컬럼타입설명
idINT PK자동증가
ratingVARCHAR(4)up / down
question / answerTEXT평가 대상 문답
commentTEXT사용자 의견 (선택)
contextJSON직전 문답 [{question, answer}] 최대 3쌍
high_qualityBOOLEAN고성능 모드 답변 여부
created_atDATETIME생성 시각

정렬 기본값: -created_at


AI 챗봇 아키텍처

순라이프 AI는 별도 Django 앱이 아니라 main 앱 안의 chatbot/ 패키지로 구현되어 있습니다.

backend/main/
├── chatbot/
│   ├── agent.py                 # LLM 에이전트 — 도구 호출, 프롬프트, 스트리밍, 답변 검증
│   └── attachment_extractor.py  # 공지 첨부파일(pdf/hwp/docx/xlsx) 다운로드·텍스트·이미지 추출
├── views/chat.py                 # DRF 뷰 — NDJSON 스트리밍 엔드포인트
└── models.py                     # ChatConversation, ChatLog, ChatFeedback

흐름

Client (fetch, 청크 읽기)
       │  POST /api/chat/

ChatView.post()
       │  sch_session 쿠키 검증 (다른 API와 동일)
       │  별도 스레드에서 Agent.run_turn() 실행 → queue로 이벤트 전달

Agent (agent.py)
       │  ├─ 도구 호출: 공지 검색 / 시맨틱 검색 / 학사일정 조회 / 첨부 조회
       │  ├─ vLLM 서버 (OpenAI 호환 /chat/completions, stream=True)
       │  └─ 답변 검증 후 확정된 텍스트만 6자 단위로 타자기 스트리밍

StreamingHttpResponse (application/x-ndjson)
  • 학생당 서버 메모리에 Agent 인스턴스 1개(+락 +중단 이벤트)를 유지하며, 진행 중인 답변 생성은 학생당 동시에 하나만 허용됩니다.
  • vLLM은 systemd로 관리되는 자체 호스팅 서버(soonlife-vllm 서비스)이며, 프롬프트 이미지 개수 제한(--limit-mm-per-prompt)에 맞춰 첨부 이미지 처리 개수가 관리됩니다.
  • 시맨틱 검색(BGE-M3 임베딩 + Qdrant + reranker)은 EMBED_SEARCH_URL이 설정된 경우에만 도구로 활성화되며, 미설정 시 공지 키워드 검색으로 대체됩니다.
  • 기숙사 규정 변경처럼 오답 위험이 큰 특정 질문 패턴은 키워드 필터 + 저비용 분류 LLM 호출로 판별해, 사람이 검증한 고정 답변으로 즉시 응답하고 일반 생성 경로를 건너뜁니다.

배포 시 gunicorn 설정

챗봇의 긴 스트리밍 응답을 지원하기 위해 gunicorn을 sync 워커에서 gthread로 변경했습니다.

gunicorn portal.wsgi:application \
    --workers 3 --worker-class gthread --threads 8 --timeout 600

sync 워커는 요청 처리 중 하트비트를 보내지 못해 챗봇처럼 오래 걸리는 스트리밍 응답이 기존 타임아웃(120초)에 강제 종료되는 문제가 있었습니다. gthread는 메인 스레드가 하트비트를 유지해 긴 응답이 끊기지 않고, 동시 처리량도 3(워커) → 24(워커×스레드)로 늘어 챗봇 응답 중에도 다른 포털 API 요청이 막히지 않습니다.


프론트엔드 구조

라우팅

App.tsx에서 React Router v7로 관리. 모든 페이지 컴포넌트는 lazy()로 코드 스플리팅됩니다.

/                    → HomePage (대시보드)
/login               → LoginPage
/academic/status     → AcademicStatusPage
/grades/cumulative   → CumulativeGradesPage
/grades/semester     → SemesterGradesPage
/timetable           → (시간표 관련 여러 경로)
/scholarship         → ScholarshipPage
/dorm/bill           → DormBillPage
...

메뉴 관리 (sitemap.ts)

상수설명
SITEMAP3개 그룹, 17개 섹션, 전체 메뉴 아이템 정의
IMPLEMENTED_ITEMS현재 구현된 메뉴 이름 Set — 미포함 항목은 사이드바에서 비활성화
ITEM_ROUTES메뉴 이름 → URL 경로 매핑
SECTION_ROUTES섹션 ID → URL 경로 매핑

새 기능 추가 체크리스트:

  1. SITEMAP에 메뉴 항목 추가 (이미 있으면 생략)
  2. IMPLEMENTED_ITEMS Set에 항목 이름 추가
  3. ITEM_ROUTES에 경로 추가
  4. App.tsxlazy() import + <Route> 추가
  5. components/ 에 페이지 컴포넌트 생성

상태 관리

별도의 전역 상태 라이브러리 없음.

데이터관리 방식
사용자 세션GET /api/auth/me/로 초기화, 메모리에 유지
즐겨찾기API로 서버와 동기화
시간표 테마API를 통해 DB에 저장
페이지별 데이터각 컴포넌트의 useState + API 호출

보안 설계

항목구현 방식
세션 저장HttpOnly 쿠키 — JS에서 document.cookie로 읽기 불가
세션 무결성AES-256-GCM 인증 태그 — 변조 시 복호화 실패
세션 바인딩IP + User-Agent AAD — 쿠키 탈취 후 재사용 방지
CSRF 방어HttpOnly 쿠키 기반이므로 CSRF 미들웨어 비활성화 (SameSite=Lax로 CSRF 방어)
CORSCORS_ALLOWED_ORIGINS 화이트리스트 방식
클릭재킹 방어X-FRAME-OPTIONS: DENY
MIME 스니핑 방어SECURE_CONTENT_TYPE_NOSNIFF: True
Referrer 정책strict-origin-when-cross-origin
Django auth 프레임워크미사용 — django.contrib.{admin,auth,contenttypes,sessions,messages}INSTALLED_APPS/MIDDLEWARE에서 전부 제거. 인증은 처음부터 자체 sch_session 쿠키(AES-256-GCM)로만 처리하며, auth_*/django_session/django_content_type 등 불필요한 테이블 생성을 막기 위한 정리

CORS 허용 오리진

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",    # Vite 개발 서버
    "http://127.0.0.1:5173",
    "https://sch.wonchan.net",  # 프로덕션
    "http://sch.wonchan.net",
]
CORS_ALLOW_CREDENTIALS = True  # 쿠키 허용

새 배포 환경을 추가할 경우 portal/settings.py의 이 목록에 오리진을 추가하세요.


학기 코드 규칙

코드설명
111학기 (3월~8월)
212학기 (9월~2월)
12하계방학 (기숙사 한정)
22동계방학 (기숙사 한정)

백엔드의 _current_semester() 함수는 오늘 날짜를 기준으로 (yy, smt)를 자동 반환합니다:

def _current_semester() -> tuple[str, str]:
    today = date.today()
    year, month = today.year, today.month
    if 3 <= month <= 8:
        return str(year), '11'      # 1학기
    elif month >= 9:
        return str(year), '21'      # 2학기
    else:
        return str(year - 1), '21'  # 전년도 2학기 (1~2월)

On this page