{
  "openapi": "3.1.0",
  "info": {
    "title": "CAU 시험시간표 검색 API",
    "version": "2.0.0",
    "description": "지원 학교: 중앙대학교(`cau`). 모든 경로가 `/api/v1/cau/…` 접두사를 쓴다.\n\n중앙대학교 시험 시간표 공개 검색 API. 인증 없이 누구나(GET) 호출할 수 있으며 사람이든 ChatGPT든 같다. 검색 문법은 웹 검색 페이지와 동일하다: 공백·쉼표로 나눈 토큰을 AND로 검사하고, 한글은 자모·초성 부분일치, `1분반`=`01분반`=`분반 1`, `월456`=`월4,5,6`처럼 표기 변형을 흡수한다. 응답의 results는 일치 개수 그대로다(1건이면 1개, 여러 건이면 여러 개). 데이터는 학기별 시험 시간표 공지의 최신 첨부만 반영되며 하루 두 번(한국시간 09:00·21:00) 갱신된다.\n\n사람이 보는 페이지(/cau/exam, /cau/exam/{term}/{exam_type}, /cau/exam/{term}/{exam_type}/{slug})는 같은 URL에서 Accept 헤더로 협상한다: text/html이면 SSR HTML, application/json이면 이 API와 같은 JSON. 머신 클라이언트는 이 /api/v1/cau/exam/* 경로를 쓰면 된다. 이 스펙은 /api/v1/cau/exam/openapi.json 에서도 제공된다."
  },
  "servers": [
    {"url": "https://univmcp.com", "description": "운영 서버"}
  ],
  "paths": {
    "/api/v1/cau/exam/search": {
      "get": {
        "operationId": "searchExams",
        "summary": "시험 일정 검색",
        "description": "term/exam_type을 생략하면 보유한 최신 시험 하나를 대상으로 검색한다. 둘 다 지정하면 그 시험만, 하나만 지정하면 그 조건으로 걸러 검색한다.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "검색어(1~100자). 공백·쉼표 구분 AND, 자모/초성 부분일치, 분반·교시 표기 변형 지원.",
            "schema": {"type": "string", "minLength": 1, "maxLength": 100},
            "examples": {
              "course_and_section": {"value": "다빈치스포츠 2분반"},
              "period": {"value": "월456"},
              "choseong": {"value": "ㄷㅂㅊ"}
            }
          },
          {
            "name": "term",
            "in": "query",
            "required": false,
            "description": "학기. 기본 형식은 YYYY-1, YYYY-2(정규), YYYY-S(하계), YYYY-W(동계)이며 '2026년도 1학기' 같은 한글 표기도 인식한다. 생략 시 기본값 규칙에 따른다.",
            "schema": {"type": "string", "pattern": "^20[0-9]{2}-(1|2|S|W)$", "examples": ["2026-1"]}
          },
          {
            "name": "exam_type",
            "in": "query",
            "required": false,
            "description": "시험 종류. 생략 시 기본값 규칙에 따른다.",
            "schema": {"type": "string", "enum": ["midterm", "final"]}
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "최대 반환 건수(1~200). 전체 일치는 total로 알려주고 잘렸을 때 truncated가 true다.",
            "schema": {"type": "integer", "minimum": 1, "maximum": 200, "default": 200}
          }
        ],
        "responses": {
          "200": {
            "description": "검색 결과. results 길이는 실제 일치 개수와 같다(0건도 빈 배열로 온다).",
            "headers": {
              "X-Exam-Term": {"description": "실제로 검색한 학기(*는 전체)", "schema": {"type": "string"}},
              "X-Exam-Type": {"description": "실제로 검색한 시험 종류(*는 전체)", "schema": {"type": "string"}}
            },
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/SearchResponse"},
                "examples": {
                  "single": {
                    "summary": "1건 일치",
                    "value": {
                      "query": "다빈치스포츠 2분반",
                      "scope": {"term": "2026-1", "exam_type": "midterm", "defaulted": true},
                      "count": 1,
                      "total": 1,
                      "truncated": false,
                      "results": [
                        {
                          "term": "2026-1",
                          "exam_type": "midterm",
                          "course_code": null,
                          "course_name": "다빈치스포츠",
                          "section": "02",
                          "lecture_time": "월1,2,3",
                          "instructor": "정우영",
                          "exam_method": "과제물대체",
                          "exam_date": null,
                          "start_time": null,
                          "end_time": null,
                          "building": null,
                          "rooms": null,
                          "note": null,
                          "document_id": "2026-1/midterm/ge/5c92de…",
                          "notice_url": "https://ge.cau.ac.kr/board_notice_view.php?no=1571&page=1",
                          "matched_fields": ["course_name", "section"]
                        }
                      ]
                    }
                  },
                  "multiple": {
                    "summary": "여러 건 일치",
                    "value": {
                      "query": "다빈치스포츠",
                      "scope": {"term": "2026-1", "exam_type": "midterm", "defaulted": true},
                      "count": 4,
                      "total": 4,
                      "truncated": false,
                      "results": [
                        {"course_name": "다빈치스포츠", "section": "01", "lecture_time": "월4,5,6", "matched_fields": ["course_name"]},
                        {"course_name": "다빈치스포츠", "section": "02", "lecture_time": "월1,2,3", "matched_fields": ["course_name"]},
                        {"course_name": "다빈치스포츠", "section": "03", "lecture_time": "목1,2,3", "matched_fields": ["course_name"]},
                        {"course_name": "다빈치스포츠", "section": "04", "lecture_time": "화11,12,13", "matched_fields": ["course_name"]}
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {"description": "q 누락·초과 또는 잘못된 파라미터", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "INVALID_QUERY", "message": "q 파라미터는 필수입니다. (1~100자)"}}}},
          "429": {"description": "요청 한도 초과", "headers": {"Retry-After": {"description": "재시도까지 대기할 초", "schema": {"type": "integer"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "500": {"description": "서버 오류", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/api/v1/cau/exam/exams": {
      "get": {
        "operationId": "listExams",
        "summary": "보유한 시험 목록(최신 시험 확인)",
        "description": "지금 수집된 시험 시간표가 어떤 것인지 알려주는 프로브. 검색 기본값으로 쓰이는 최신 시험은 latest와 각 항목의 latest로 확인한다.",
        "responses": {
          "200": {
            "description": "학기·시험 종류별 집계. 최신 시험이 먼저 온다.",
            "headers": {
              "X-Exam-Term": {"description": "보유한 최신 시험의 학기", "schema": {"type": "string"}},
              "X-Exam-Type": {"description": "보유한 최신 시험의 종류", "schema": {"type": "string"}}
            },
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ExamsResponse"},
                "example": {
                  "latest": {"term": "2026-1", "exam_type": "final"},
                  "exams": [
                    {"term": "2026-1", "exam_type": "final", "latest": true, "sources": 3, "courses": 1196, "collected_at": "2026-10-07T00:18:15.557Z"},
                    {"term": "2026-1", "exam_type": "midterm", "latest": false, "sources": 12, "courses": 3243, "collected_at": "2026-10-07T06:44:13.576Z"}
                  ]
                }
              }
            }
          },
          "429": {"description": "요청 한도 초과", "headers": {"Retry-After": {"description": "재시도까지 대기할 초", "schema": {"type": "integer"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "500": {"description": "서버 오류", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/api/v1/cau/exam/schedules": {
      "get": {
        "operationId": "listSchedules",
        "summary": "시험 일정 원본 행(검색 위젯용)",
        "description": "한 시험의 전체 행을 그대로 내려준다. 검색 페이지가 클라이언트에서 즉시 검색하기 위해 한 번에 가져가는 데이터셋이다. 원문(raw_text)까지 포함하므로 일반적인 조회에는 search를 쓰는 편이 가볍다.",
        "parameters": [
          {"name": "term", "in": "query", "required": false, "schema": {"type": "string", "pattern": "^20[0-9]{2}-(1|2|S|W)$"}},
          {"name": "exam_type", "in": "query", "required": false, "schema": {"type": "string", "enum": ["midterm", "final"]}}
        ],
        "responses": {
          "200": {
            "description": "시험 일정 행 목록(정렬된 순서 그대로).",
            "content": {"application/json": {"schema": {"type": "object", "required": ["schedules"], "properties": {"schedules": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduleRow"}}}}}}
          },
          "429": {"description": "요청 한도 초과", "headers": {"Retry-After": {"description": "재시도까지 대기할 초", "schema": {"type": "integer"}}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "500": {"description": "서버 오류", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SearchScope": {
        "type": "object",
        "description": "실제로 검색한 범위. defaulted가 true면 파라미터 없이 최신 시험을 기본값으로 쓴 것이다.",
        "required": ["term", "exam_type", "defaulted"],
        "properties": {
          "term": {"type": ["string", "null"]},
          "exam_type": {"type": ["string", "null"], "enum": ["midterm", "final", null]},
          "defaulted": {"type": "boolean"}
        }
      },
      "SearchResult": {
        "type": "object",
        "required": ["term", "exam_type", "course_code", "course_name", "section", "lecture_time", "instructor", "exam_method", "exam_date", "start_time", "end_time", "building", "rooms", "note", "document_id", "notice_url", "matched_fields"],
        "properties": {
          "term": {"type": "string", "examples": ["2026-1"]},
          "exam_type": {"type": "string", "enum": ["midterm", "final"]},
          "course_code": {"type": ["string", "null"], "examples": ["42376"]},
          "course_name": {"type": "string"},
          "section": {"type": ["string", "null"], "examples": ["02"]},
          "lecture_time": {"type": ["string", "null"], "description": "정규화된 강의시간(교시 통일 표기). 교시 n은 (n+8):00–(n+8):50. 예: 월1,2,3, 화1,2 / 목1,2. 시각 표기('화(09:00~10:15)')는 걸치는 교시로 변환된다. 미상이면 null.", "examples": ["화1,2 / 목1,2"]},
          "instructor": {"type": ["string", "null"]},
          "exam_method": {"type": ["string", "null"], "examples": ["지필", "온라인시험", "과제물대체"]},
          "exam_date": {"type": ["string", "null"], "description": "YYYY-MM-DD. 미정이면 null.", "examples": ["2026-06-22"]},
          "start_time": {"type": ["string", "null"], "examples": ["09:00"]},
          "end_time": {"type": ["string", "null"], "examples": ["10:30"]},
          "building": {"type": ["string", "null"], "description": "건물 N관 번호(숫자). 예: 203관(서라벌홀) → 203", "examples": ["203"]},
          "rooms": {"type": ["string", "null"], "description": "호실 표기(건물 번호 미포함). 예: 511호 <강의실>", "examples": ["511호 <강의실>"]},
          "note": {"type": ["string", "null"]},
          "document_id": {"type": "string", "description": "원본 문서 ID. /download?id= 에 그대로 쓸 수 있다."},
          "notice_url": {"type": ["string", "null"], "format": "uri", "description": "원본 공지 URL"},
          "matched_fields": {"type": "array", "description": "검색어가 실제로 매칭된 필드", "items": {"type": "string", "enum": ["course_name", "section", "lecture_time", "instructor", "course_code"]}}
        }
      },
      "ScheduleRow": {
        "type": "object",
        "description": "검색 결과 필드에서 matched_fields를 뺀 뒤 원문(raw_text)과 상세 페이지 슬러그(slug)를 더한 형태.",
        "required": ["term", "exam_type", "course_code", "course_name", "section", "lecture_time", "instructor", "exam_method", "exam_date", "start_time", "end_time", "building", "rooms", "note", "document_id", "notice_url", "raw_text", "slug"],
        "properties": {
          "term": {"type": "string"},
          "exam_type": {"type": "string", "enum": ["midterm", "final"]},
          "course_code": {"type": ["string", "null"]},
          "course_name": {"type": "string"},
          "section": {"type": ["string", "null"]},
          "lecture_time": {"type": ["string", "null"], "description": "정규화된 강의시간(교시 통일 표기). 예: 월1,2,3, 화1,2 / 목1,2"},
          "instructor": {"type": ["string", "null"]},
          "exam_method": {"type": ["string", "null"]},
          "exam_date": {"type": ["string", "null"]},
          "start_time": {"type": ["string", "null"]},
          "end_time": {"type": ["string", "null"]},
          "building": {"type": ["string", "null"], "description": "건물 N관 번호(숫자). 예: 203관(서라벌홀) → 203", "examples": ["203"]},
          "rooms": {"type": ["string", "null"], "description": "호실 표기(건물 번호 미포함). 예: 511호 <강의실>", "examples": ["511호 <강의실>"]},
          "note": {"type": ["string", "null"]},
          "document_id": {"type": "string"},
          "notice_url": {"type": ["string", "null"], "format": "uri"},
          "raw_text": {"type": ["string", "null"], "description": "파서가 읽은 원문 행 텍스트"},
          "slug": {"type": "string", "description": "상세 페이지 /cau/exam/{term}/{exam_type}/{slug} 의 슬러그"}
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": ["query", "scope", "count", "total", "truncated", "results"],
        "properties": {
          "query": {"type": "string"},
          "scope": {"$ref": "#/components/schemas/SearchScope"},
          "count": {"type": "integer", "description": "반환된 결과 수"},
          "total": {"type": "integer", "description": "전체 일치 수"},
          "truncated": {"type": "boolean", "description": "limit으로 잘렸는지 여부"},
          "results": {"type": "array", "items": {"$ref": "#/components/schemas/SearchResult"}}
        }
      },
      "ExamKey": {
        "type": "object",
        "required": ["term", "exam_type"],
        "properties": {
          "term": {"type": "string"},
          "exam_type": {"type": "string", "enum": ["midterm", "final"]}
        }
      },
      "ExamInfo": {
        "type": "object",
        "required": ["term", "exam_type", "latest", "sources", "courses", "collected_at"],
        "properties": {
          "term": {"type": "string"},
          "exam_type": {"type": "string", "enum": ["midterm", "final"]},
          "latest": {"type": "boolean", "description": "검색 기본값으로 쓰이는 최신 시험인지"},
          "sources": {"type": "integer", "description": "집계된 원본 공고 수"},
          "courses": {"type": "integer", "description": "검색 가능한 시험 일정 행 수"},
          "collected_at": {"type": ["string", "null"], "description": "원본 수집 시각(ISO 8601)"}
        }
      },
      "ExamsResponse": {
        "type": "object",
        "required": ["latest", "exams"],
        "properties": {
          "latest": {"oneOf": [{"$ref": "#/components/schemas/ExamKey"}, {"type": "null"}]},
          "exams": {"type": "array", "items": {"$ref": "#/components/schemas/ExamInfo"}}
        }
      },
      "Error": {
        "type": "object",
        "required": ["error", "message"],
        "properties": {
          "error": {"type": "string", "enum": ["INVALID_QUERY", "INVALID_PARAM", "METHOD_NOT_ALLOWED", "NOT_FOUND", "RATE_LIMITED", "INTERNAL"]},
          "message": {"type": "string"}
        }
      }
    }
  }
}
