Skip to content

Core Domain Flows

kimhoeyun edited this page Sep 8, 2026 · 6 revisions

Core Domain Flows

포털 연동과 졸업 진단처럼 여러 컴포넌트가 연결되는 핵심 처리 흐름을 설명합니다.

포털 연동

sequenceDiagram
    participant C as Client
    participant B as Backend
    participant D as PostgreSQL
    participant Q as SQS
    participant W as Worker
    participant P as Portal
    participant S as S3

    C->>B: POST /portal/link + Idempotency-Key
    B->>D: scrape_jobs + outbox 저장
    B->>Q: Outbox payload 발행
    B-->>C: job_id + polling_endpoint
    W->>Q: 작업 수신
    W->>P: 포털 로그인 및 데이터 조회
    W->>S: 결과 JSON 저장
    W->>B: POST /internal/scrape-results + HMAC
    B->>S: 결과 JSON 조회 및 checksum 검증
    B->>D: 학사 데이터와 작업 상태 갱신
    C->>B: GET /portal/link/jobs/{jobId}
    B-->>C: 작업 상태
Loading

요청 수락

POST /portal/link는 인증된 사용자와 Idempotency-Key를 요구합니다. 현재 지원하는 portal_typesuwon입니다.

백엔드는 사용자의 기존 연동 여부에 따라 작업을 LINK 또는 REFRESH로 분류합니다. 같은 사용자가 같은 Idempotency-Key와 같은 요청을 다시 보내면 기존 작업을 반환합니다. 같은 키로 다른 요청을 보내면 C07 IDEMPOTENCY_KEY_CONFLICT입니다.

응답에는 다음 값이 들어갑니다.

  • job_id.
  • status=accepted.
  • /portal/link/jobs/{jobId} 형태의 polling_endpoint.

작업과 Outbox 상태

작업 상태는 다음 순서로 진행됩니다.

상태 의미
QUEUED 요청과 Outbox가 저장됨
RUNNING Worker가 작업을 처리 중
POST_PROCESSING 결과를 받아 학사 데이터를 반영 중
SUCCEEDED 데이터 반영 완료
FAILED 발행, Worker, 콜백, 후처리 중 실패

Outbox 상태는 PENDING, SENT, RETRYABLE_FAILED, DEAD입니다. 요청 수락 경로는 생성된 Outbox를 동기적으로 SQS에 발행하고, 발행 결과가 SENT가 아니면 요청을 C10으로 실패시킵니다.

현재 코드에는 dispatchEligibleOutboxes()가 있지만 이를 주기적으로 호출하는 운영 경로가 없습니다. RETRYABLE_FAILED가 자동으로 재처리된다고 가정하면 안 됩니다. 실패 원인을 해결한 뒤 새 요청으로 전체 흐름을 검증합니다.

상태 조회

  • GET /portal/link/jobs/{jobId}는 현재 상태와 오류 정보를 반환합니다.
  • GET /portal/link/jobs/{jobId}/summarySUCCEEDED일 때만 학생 요약을 반환합니다.
  • GET /portal/link/jobs/{jobId}/duration은 시작·종료 시각과 소요 시간을 반환합니다.

다른 사용자의 job_id는 조회할 수 없습니다.

결과 콜백

Worker는 POST /internal/scrape-results로 콜백합니다.

필수 Header입니다.

  • X-Timestamp.
  • X-Signature.

진단에 사용하는 선택 Header입니다.

  • X-Callback-Attempt.
  • X-Request-Id.

서명 canonical string은 X-Timestamp + "." + 원문 Body입니다. 이 문자열을 SCRAPING_CALLBACK_HMAC_SECRET으로 HMAC-SHA256 처리합니다. JSON을 다시 직렬화한 값으로 서명하면 원문이 달라져 검증에 실패할 수 있습니다.

성공 콜백은 result_s3_key를 요구합니다. 백엔드는 Bucket과 Prefix가 설정 범위 안에 있는지, Key가 해당 job_id 범위인지 확인합니다. 그다음 S3 결과를 읽어 선택적으로 SHA-256 checksum을 검증합니다.

동일한 콜백이 다시 와도 처리 결과를 중복 적재하지 않습니다. 중복 콜백은 scrape.job.callback.duplicate metric과 로그로 확인할 수 있습니다.

결과 후처리

S3 결과의 snake_case key는 내부 처리 전에 camelCase로 정규화됩니다. 후처리는 다음 데이터를 갱신합니다.

  • 사용자 포털 연동 상태.
  • 학생 프로필과 학적 정보.
  • 학기별 성적과 수강 과목.
  • 과목, 교수, 학과 기준 정보.
  • 졸업 진행도 계산에 필요한 데이터.

포털 payload에 designatedCourses가 포함되면 지정과목 원본도 같은 후처리 트랜잭션에서 갱신합니다. 이 목록은 지정과목 안내 정보이며 이수 내역이 아니므로 student_courses나 취득학점에 직접 반영하지 않습니다.

  • 필드가 없거나 null이면 구버전 scraper 결과로 보고 기존 목록과 스냅샷 버전을 유지합니다.
  • 빈 배열이면 기존 목록을 모두 삭제하고 스냅샷 버전을 갱신합니다.
  • 값이 있는 배열이면 원본 순서와 중복을 보존해 전체 교체합니다.
  • ScrapeJob.createdAt보다 오래된 결과는 학생 잠금과 스냅샷 버전 비교 후 무시합니다.

결과 스키마가 잘못되면 C17, S3를 읽지 못하면 C16, DB 반영 중 실패하면 C18로 작업이 실패합니다.

졸업 진행도 조회

GET /api/graduation/progress는 포털에서 적재한 학생별 학점과 졸업 요건 정책을 조합합니다.

flowchart LR
    Student["학생 학적\n입학년도·학과·전공"]
    Courses["학생별 이수 과목과 학점"]
    Rules["졸업·영역·복수전공·어학 요건"]
    Service["Graduation Service"]
    Result["충족 여부와 부족 항목"]

    Student --> Service
    Courses --> Service
    Rules --> Service
    Service --> Result
Loading

일반 재학생의 영역별 이수 학점은 student_courses와 졸업 요건을 비교해 계산합니다. 공통 개설 과목의 학점만으로 개인 이수 학점을 대체하면 재수강이나 학점 보정 결과가 틀릴 수 있습니다.

편입생은 일반 재학생의 영역별 요건을 그대로 계산하지 않습니다. 응답은 analysisType=TRANSFER, analysisStatus=MANUAL_REVIEW_REQUIRED로 반환하고 다음 자동 계산 결과를 transferProgress에 담습니다.

  • totalEarnedCreditsstudent_academic_records.total_earned_credits에 저장된 포털 누적 취득학점 단일 기준입니다.
  • recognizedTransferCredits는 편입 인정 과목 코드와 학생 수강 기록을 비교해 계산하며, 이 값을 totalEarnedCredits에 다시 더하지 않습니다.
  • cumulativeGpa와 적용 최소 GPA를 비교해 GPA 충족 여부를 계산합니다. 복수전공이 있으면 현재 최소 GPA는 2.5입니다.
  • designatedCoursesstudent_designated_courses의 원본 지정과목과 실제 student_courses를 과목 코드로 비교해 COMPLETED, NOT_COMPLETED, UNKNOWN 상태를 제공합니다. 지정과목 원본의 point는 자동 취득학점으로 합산하지 않습니다.
  • 필수과목, 전선 비율, 부전공·연계전공, 졸업심사 등 포털 데이터만으로 확정할 수 없는 항목은 manualReviewReasons에 남깁니다.

지정과목 목록은 안내·판정 대상 데이터일 뿐이며, 지정과목 행을 저장했다고 해서 취득학점이나 일반 영역별 학점에 직접 합산하지 않습니다. 편입 인정학점도 누적 취득학점과 중복 합산하지 않습니다.

학생, 학적 데이터, 적용 가능한 졸업 요건이 없으면 정상 계산이 불가능합니다. 이 경우 포털 연동 완료 여부와 학생의 입학년도·학과·전공을 먼저 확인합니다.

편입 전핵·전선 기준 조회 — PR #343.

PR #343은 기존 부분 진단에 아래 비교 경로를 연결합니다. 병합·배포 전에는 기존 실행 버전과 동작이 다를 수 있습니다.

  1. 저장된 편입 입학연도에서 2년을 빼 적용 교육과정 연도를 구합니다. 현재 재학 학년이 올라가도 기준 연도는 바뀌지 않습니다.
  2. 기존 학과 resolver로 주전공 우선·학과명 변경 매핑을 적용하고 department_area_requirements의 학과·기준 연도별 전핵·전선 required_credits를 조회합니다.
  3. 각 기준에 정확히 0.5를 곱합니다. 유효 성적·재수강 삭제·중복 정규화를 거친 해당 영역의 개인 취득학점이 기준 이상이면 충족입니다. 편입 인정학점 코드는 영역 합계에서 제외합니다.
  4. 한 영역의 기준이 없거나 서로 다른 요구학점 행이 중복되면 그 영역만 미확인으로 남깁니다. 같은 요구학점의 중복은 한 기준으로 사용합니다. 적용 연도·학과·요건이 없거나 복수전공 정책이 미확정이면 비교를 보류하고 확인 가능한 이수 내역은 제공합니다.

전핵 전체 필수과목 목록과 3·4학년 과목별 이수 판정은 사용하지 않습니다. 기존 CORE_CURRICULUM_UNAVAILABLE 코드는 호환을 위해 유지하며 이제 해당 교육과정의 전핵 학점 기준 미확인을 뜻합니다. 누적 총학점·GPA·외국어 인증 및 지정과목 원본 저장 규칙은 유지합니다.

같은 과목의 최신 유효 수강 기록은 연도·학기로만 선택하며 원점수로 최신 여부를 결정하지 않습니다. 최신 학기 안에서 개인 학점·성적등급·이수구분이 다르면 충돌로 표시하고 해당 과목이 포함된 영역의 학점 합계와 충족 여부를 미확인으로 둡니다. 더 최신 학기의 기록이 있으면 과거 학기의 충돌은 해제하며, 같은 학기의 일치 기록이 추가되는 것만으로는 충돌을 해제하지 않습니다.

강의평가

강의평가는 대상 학기와 과목을 기준으로 필요 여부를 판단합니다. 상태는 학기 학적 데이터에 저장되며 PENDING, SKIPPED, COMPLETED, NOT_RELEASED 등의 흐름을 사용합니다. 대상 학기나 제출 과목이 맞지 않으면 A07, A08 오류를 확인합니다.

Clone this wiki locally