방과후 운영 한눈에 API 연동 문서
대상: Next.js 기반 웹 서비스
인증 방식: 로그인 후 발급된 세션 또는 접근 토큰을 요청에 포함한다.
공통 원칙:
- 모든 운영자 요청은 로그인한 사용자가 해당 위탁업체의 운영자 권한을 가졌는지 확인한다.
- 모든 강사 요청은 로그인한 계정이 해당 위탁업체의 해당 강사 정보와 연결되었는지 확인한다.
- 위탁업체 식별값은 로그인 정보에서 확인하며, 화면이 다른 위탁업체의 식별값을 임의로 보내 다른 업체 데이터를 조회·수정할 수 없다. (NFR-001)
- 날짜는
YYYY-MM-DD, 시각은HH:mm형식으로 처리한다. 시간대는 대한민국 표준시를 사용한다. (NFR-003) - 원본 서류 파일, 서류 상태, 대체강사, 정산, 민원 데이터는 첫 개발 범위에 포함하지 않는다. (NFR-005)
공통 응답 형식
성공 응답
{
"data": {}
}
오류 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력한 내용을 다시 확인해 주세요.",
"fields": [
{
"name": "schoolName",
"message": "학교명을 입력해 주세요."
}
]
}
}
화면에 표시할 주요 오류 코드
| 오류 코드 | 사용자가 이해할 수 있는 안내 |
|---|---|
UNAUTHORIZED | 로그인 시간이 만료되었습니다. 다시 로그인해 주세요. |
FORBIDDEN | 이 정보에 접근할 권한이 없습니다. |
NOT_FOUND | 요청한 정보를 찾을 수 없습니다. |
VALIDATION_ERROR | 빠졌거나 올바르지 않은 입력값이 있습니다. 표시된 항목을 확인해 주세요. |
DUPLICATE_TIME_OVERLAP | 같은 강사가 서로 다른 학교의 겹치는 시간에 배정되어 저장할 수 없습니다. |
SAME_SCHOOL_EXCEPTION_REASON_REQUIRED | 같은 학교 시간 겹침을 저장하려면 예외 사유를 입력해 주세요. |
CONCURRENT_UPDATE | 다른 사용자가 먼저 변경했습니다. 최신 내용을 확인한 뒤 다시 저장해 주세요. |
FILE_FORMAT_ERROR | 지정 엑셀 양식 또는 지원하는 파일 형식인지 확인해 주세요. |
INVITATION_INVALID | 초대 링크가 유효하지 않거나 이미 사용할 수 없습니다. |
PUBLISH_BLOCKED | 해결되지 않은 시간 중복 또는 예외 사유 누락이 있어 공개할 수 없습니다. |
1. 계정 시작과 로그인
POST /api/agencies
목적: 대표가 운영자 계정과 위탁업체를 함께 생성한다. (FR-001, UIR-001)
인증: 불필요
HTTP 방식과 경로: POST /api/agencies
요청값
{
"ownerName": "홍길동",
"email": "owner@example.com",
"password": "비밀번호",
"agencyName": "방과후 운영 한눈에 교육",
"phone": "010-1234-5678"
}
| 값 | 필수 | 설명 |
|---|---|---|
ownerName | 예 | 대표 이름 |
email | 예 | 로그인에 사용할 이메일 |
password | 예 | 로그인 비밀번호 |
agencyName | 예 | 위탁업체 이름 |
phone | 아니오 | 위탁업체 연락처 |
성공 응답
{
"data": {
"agencyId": "agency_001",
"userId": "user_001",
"role": "operator",
"redirectPath": "/operator/dashboard"
}
}
사용자 오류 상황
- 필수 입력값이 비어 있으면 해당 입력칸 아래에 안내한다.
- 이미 사용 중인 이메일이면 기존 계정으로 로그인하도록 안내한다.
- 계정 생성 중 위탁업체 생성에 실패하면 계정 또는 위탁업체가 일부만 남지 않도록 전체 생성을 취소한다. (FR-001, NFR-002)
POST /api/auth/login
목적: 운영자 또는 강사가 로그인하고 역할에 맞는 첫 화면으로 이동한다. (FR-002, UIR-001, UIR-008)
인증: 불필요
HTTP 방식과 경로: POST /api/auth/login
요청값
{
"email": "instructor@example.com",
"password": "비밀번호"
}
성공 응답
{
"data": {
"userId": "user_020",
"role": "instructor",
"agencyId": "agency_001",
"redirectPath": "/instructor/timetable",
"invitationPending": false
}
}
사용자 오류 상황
- 이메일 또는 비밀번호가 맞지 않으면 “로그인 정보를 확인해 주세요.”라고 표시한다.
- 계정 존재 여부를 구분해 알려주지 않는다.
- 강사 초대 연결이 끝나지 않았다면 개인 시간표 대신 초대 연결 화면으로 이동시킨다. (FR-002)
POST /api/auth/logout
목적: 현재 로그인 상태를 종료한다. (FR-002)
인증: 필요
HTTP 방식과 경로: POST /api/auth/logout
요청값
없음
성공 응답
{
"data": {
"loggedOut": true
}
}
사용자 오류 상황
- 이미 로그인 시간이 끝난 경우에도 로그인 화면으로 이동시킨다.
GET /api/auth/me
목적: 현재 로그인한 사용자, 역할, 연결된 위탁업체 및 강사 연결 상태를 확인한다. 화면 접근 권한을 판단할 때 사용한다. (FR-002, FR-015)
인증: 필요
HTTP 방식과 경로: GET /api/auth/me
요청값
없음
성공 응답
{
"data": {
"userId": "user_020",
"role": "instructor",
"agencyId": "agency_001",
"instructorId": "instructor_014",
"invitationPending": false
}
}
사용자 오류 상황
- 로그인하지 않았거나 로그인 시간이 만료된 경우 로그인 화면으로 이동한다.
- 운영자 화면에 강사가 접근하면 접근을 차단한다. (FR-002, NFR-004)
2. 운영자 기본 화면
GET /api/operator/dashboard
목적: 운영자가 로그인 후 학교·강사 등록, 수업 등록, 전체 시간표 확인으로 바로 이동할 수 있는 시작 정보를 제공한다. (FR-001, FR-003, FR-004, FR-013, UIR-002)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/operator/dashboard
요청값
없음
성공 응답
{
"data": {
"agency": {
"agencyId": "agency_001",
"agencyName": "방과후 운영 한눈에 교육"
},
"summary": {
"schoolCount": 20,
"instructorCount": 35,
"classPlanCount": 120,
"unresolvedDifferentSchoolOverlapCount": 2,
"sameSchoolOverlapExceptionCount": 1,
"unpublishedClassPlanCount": 18
},
"quickActions": [
"학교 등록",
"강사 등록",
"엑셀 시간표 업로드",
"수업 직접 등록",
"전체 시간표 보기"
]
}
}
사용자 오류 상황
- 운영자 권한이 없으면 운영자 화면을 열 수 없다고 안내한다.
- 아직 학교나 강사가 없으면 빈 상태와 함께 등록 버튼을 표시한다. (UIR-002)
3. 학교와 강사 관리
GET /api/schools
목적: 위탁업체가 등록한 학교 목록을 보여준다. (FR-003, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/schools
요청값
| 값 | 필수 | 설명 |
|---|---|---|
keyword | 아니오 | 학교명 검색어 |
page | 아니오 | 페이지 번호 |
pageSize | 아니오 | 한 화면에 표시할 수 |
성공 응답
{
"data": {
"items": [
{
"schoolId": "school_001",
"schoolName": "새봄초등학교",
"semesterCount": 2,
"classPlanCount": 12
}
],
"page": 1,
"pageSize": 20,
"totalCount": 1
}
}
사용자 오류 상황
- 검색 결과가 없으면 현재 검색 조건과 “학교 등록” 버튼을 함께 보여준다.
POST /api/schools
목적: 운영자가 새로운 학교를 등록한다. (FR-003, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/schools
요청값
{
"schoolName": "새봄초등학교"
}
성공 응답
{
"data": {
"schoolId": "school_001",
"schoolName": "새봄초등학교"
}
}
사용자 오류 상황
- 학교명이 비어 있으면 저장하지 않는다.
- 같은 이름의 학교가 있으면 기존 학교인지 확인하도록 경고한다.
- 같은 이름이라도 운영자가 새 학교 등록을 명확히 선택한 경우에는 별도 학교로 저장할 수 있다. 학교 구분은 이름이 아닌 학교 고유 식별값을 기준으로 한다. (FR-003)
PATCH /api/schools/{schoolId}
목적: 운영자가 학교의 표시 정보를 수정한다. (FR-003, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: PATCH /api/schools/{schoolId}
요청값
{
"schoolName": "새봄초등학교"
}
성공 응답
{
"data": {
"schoolId": "school_001",
"schoolName": "새봄초등학교",
"updatedAt": "2026-08-25T10:00:00+09:00"
}
}
사용자 오류 상황
- 학교명이 비어 있으면 저장하지 않는다.
- 학교명을 바꿔도 기존 수업 계획은 같은 학교 고유 식별값에 연결된 상태로 유지한다. (FR-003)
GET /api/instructors
목적: 위탁업체가 등록한 강사 목록과 강사 초대 상태를 조회한다. (FR-004, FR-015, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/instructors
요청값
| 값 | 필수 | 설명 |
|---|---|---|
keyword | 아니오 | 강사명 검색어 |
invitationStatus | 아니오 | not_invited, invited, connected 중 하나 |
page | 아니오 | 페이지 번호 |
pageSize | 아니오 | 한 화면에 표시할 수 |
성공 응답
{
"data": {
"items": [
{
"instructorId": "instructor_014",
"instructorName": "김미술",
"invitationStatus": "connected",
"classPlanCount": 4
}
],
"page": 1,
"pageSize": 20,
"totalCount": 35
}
}
사용자 오류 상황
- 같은 이름의 강사가 여러 명이어도 각각 별도 항목으로 보여준다.
- 이름만으로 강사를 자동으로 하나 선택하지 않는다. (FR-004)
POST /api/instructors
목적: 운영자가 강사 정보를 등록한다. (FR-004, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/instructors
요청값
{
"instructorName": "김미술",
"phone": "010-9876-5432",
"email": "instructor@example.com"
}
| 값 | 필수 | 설명 |
|---|---|---|
instructorName | 예 | 강사 표시 이름 |
phone | 아니오 | 운영자 확인용 연락처 |
email | 아니오 | 강사 초대 시 사용할 이메일 |
성공 응답
{
"data": {
"instructorId": "instructor_014",
"instructorName": "김미술",
"invitationStatus": "not_invited"
}
}
사용자 오류 상황
- 강사 이름이 비어 있으면 저장하지 않는다.
- 같은 이름의 강사는 저장할 수 있으나, 이후 엑셀 업로드에서 운영자가 직접 구분해야 한다. (FR-004, FR-008)
PATCH /api/instructors/{instructorId}
목적: 운영자가 강사 표시 정보를 수정한다. (FR-004, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: PATCH /api/instructors/{instructorId}
요청값
{
"instructorName": "김미술",
"phone": "010-9876-5432",
"email": "instructor@example.com"
}
성공 응답
{
"data": {
"instructorId": "instructor_014",
"instructorName": "김미술",
"updatedAt": "2026-08-25T10:00:00+09:00"
}
}
사용자 오류 상황
- 존재하지 않는 강사를 수정하려 하면 목록을 새로고침하도록 안내한다.
- 다른 위탁업체의 강사를 수정하려는 요청은 차단한다. (FR-004, NFR-001)
4. 학교별 학기와 교시 템플릿
GET /api/schools/{schoolId}/semesters
목적: 선택한 학교에 등록된 학기 목록을 조회한다. (FR-005, UIR-004)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/schools/{schoolId}/semesters
요청값
없음
성공 응답
{
"data": {
"items": [
{
"semesterId": "semester_2026_2",
"semesterName": "2026년 2학기",
"startDate": "2026-08-20",
"endDate": "2027-02-10"
}
]
}
}
사용자 오류 상황
- 해당 학교를 찾을 수 없으면 학교 목록으로 돌아가도록 안내한다.
POST /api/schools/{schoolId}/semesters
목적: 학교별 학기를 등록한다. (FR-005, UIR-004)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/schools/{schoolId}/semesters
요청값
{
"semesterName": "2026년 2학기",
"startDate": "2026-08-20",
"endDate": "2027-02-10"
}
성공 응답
{
"data": {
"semesterId": "semester_2026_2",
"semesterName": "2026년 2학기",
"startDate": "2026-08-20",
"endDate": "2027-02-10"
}
}
사용자 오류 상황
- 종료일이 시작일보다 빠르면 저장하지 않는다.
- 학기 이름, 시작일 또는 종료일이 빠지면 해당 항목을 표시한다. (FR-005)
GET /api/semesters/{semesterId}/period-templates
목적: 학교·학기에 적용 중인 교시 템플릿과 요일별 교시표를 조회한다. (FR-005, UIR-004)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/semesters/{semesterId}/period-templates
요청값
없음
성공 응답
{
"data": {
"items": [
{
"periodTemplateId": "template_001",
"templateName": "기본 교시표",
"startDate": "2026-08-20",
"endDate": "2027-02-10",
"periodTable": [
{
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20"
}
]
}
]
}
}
사용자 오류 상황
- 교시 템플릿이 없으면 수업 직접 등록 전에 교시표를 등록하도록 안내한다.
- 직접 실제 시작·종료 시각을 입력하는 수업은 교시 템플릿 없이도 등록할 수 있다. (FR-005, FR-009)
POST /api/semesters/{semesterId}/period-templates
목적: 운영자가 기본 교시표 또는 단축수업용 교시 템플릿을 등록한다. (FR-005, UIR-004)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/semesters/{semesterId}/period-templates
요청값
{
"templateName": "기본 교시표",
"startDate": "2026-08-20",
"endDate": "2027-02-10",
"periodTable": [
{
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20"
}
]
}
성공 응답
{
"data": {
"periodTemplateId": "template_001",
"templateName": "기본 교시표",
"startDate": "2026-08-20",
"endDate": "2027-02-10"
}
}
사용자 오류 상황
- 종료 시각이 시작 시각과 같거나 빠르면 해당 교시를 저장하지 않는다.
- 적용 종료일이 적용 시작일보다 빠르면 저장하지 않는다.
- 같은 학교·학기·요일·적용 기간에 둘 이상의 교시 템플릿이 적용되어 어느 교시표를 써야 하는지 판단할 수 없으면 저장하지 않는다. (FR-005, NFR-003)
PATCH /api/period-templates/{periodTemplateId}
목적: 교시 템플릿과 교시표를 수정하고, 영향받는 수업 수를 확인한다. (FR-005, UIR-004)
인증: 운영자 필요
HTTP 방식과 경로: PATCH /api/period-templates/{periodTemplateId}
요청값
{
"templateName": "단축수업 교시표",
"startDate": "2026-10-01",
"endDate": "2026-10-02",
"periodTable": [
{
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:30",
"endTime": "11:00"
}
],
"confirmAffectedClassPlans": true
}
성공 응답
{
"data": {
"periodTemplateId": "template_001",
"affectedClassPlanCount": 6,
"requiresOverlapRecheck": true
}
}
사용자 오류 상황
- 기존 수업 계획에 영향을 주는 변경이면 영향받는 수업 수를 먼저 보여주고 운영자 확인 후 반영한다.
- 공개된 수업의 실제 시간이 달라지는 경우 시간 중복 검사를 다시 통과해야 한다.
- 충돌이 생기면 공개 상태를 유지한 채 변경을 완료하지 않고, 충돌 수정 화면으로 이동할 수 있게 한다. (FR-005, FR-010, FR-011)
5. 엑셀 시간표 등록
GET /api/class-plans/import-template
목적: 운영자가 지정 엑셀 시간표 양식을 내려받는다. (FR-006, UIR-005)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/class-plans/import-template
요청값
없음
성공 응답
엑셀 파일을 내려받는다. 파일에는 다음 열과 입력 예시를 포함한다.
| 열 이름 | 설명 |
|---|---|
| 학교명 | 이미 등록된 학교명 |
| 학기 | 학교에 등록된 학기 |
| 운영 시작일 | 수업 반복 시작일 |
| 운영 종료일 | 수업 반복 종료일 |
| 요일 | 월~일 |
| 과목 | 담당 과목 |
| 강사명 | 등록된 강사명 |
| 교시 | 선택 입력 |
| 실제 시작 시각 | 교시 대신 직접 입력 가능 |
| 실제 종료 시각 | 교시 대신 직접 입력 가능 |
사용자 오류 상황
- 파일 생성에 실패하면 빈 파일을 제공하지 않는다.
- “양식을 만들지 못했습니다. 잠시 후 다시 시도해 주세요.”라고 안내한다. (FR-006)
POST /api/class-plans/imports
목적: 엑셀 파일을 실제 시간표에 저장하지 않고 임시로 읽어 미리보기 데이터를 만든다. (FR-007, UIR-006)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans/imports
요청 형식: multipart/form-data
요청값
| 값 | 필수 | 설명 |
|---|---|---|
file | 예 | 지정 양식으로 작성한 엑셀 파일 |
성공 응답
{
"data": {
"importId": "import_001",
"status": "preview_ready",
"totalRowCount": 120,
"normalRowCount": 110,
"warningRowCount": 5,
"errorRowCount": 5,
"rows": [
{
"rowId": "row_001",
"rowNumber": 2,
"status": "error",
"values": {
"schoolName": "새봄초등학교",
"semesterName": "2026년 2학기",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"instructorName": "김미술"
},
"errors": [
{
"field": "instructorName",
"message": "같은 이름의 강사가 2명입니다. 올바른 강사를 선택해 주세요."
}
],
"warnings": []
}
]
}
}
사용자 오류 상황
- 지원하지 않는 파일 형식이거나 필수 열을 읽을 수 없으면 업로드를 중단한다.
- 일부 행을 읽지 못하면 행 번호와 원인을 표에 표시한다.
- 파일 전체를 읽을 수 없으면 기존 수업 계획에는 어떤 데이터도 저장하지 않는다.
- 업로드한 파일은 검증과 확정에 필요한 기간에만 보관하고, 확정하지 않은 임시 데이터는 삭제 정책을 적용한다. 세부 보관 기간은 확인 필요다. (FR-007, NFR-006)
PATCH /api/class-plans/imports/{importId}/rows/{rowId}
목적: 운영자가 엑셀 미리보기에서 오류 또는 경고가 있는 행을 직접 수정한다. (FR-008, UIR-006)
인증: 운영자 필요
HTTP 방식과 경로: PATCH /api/class-plans/imports/{importId}/rows/{rowId}
요청값
{
"schoolId": "school_001",
"semesterId": "semester_2026_2",
"instructorId": "instructor_014",
"operatingStartDate": "2026-08-20",
"operatingEndDate": "2027-02-10",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"timeSource": "period_template"
}
| 값 | 필수 | 설명 |
|---|---|---|
schoolId | 예 | 화면에서 선택한 학교 고유 식별값 |
semesterId | 예 | 선택한 학기 고유 식별값 |
instructorId | 예 | 동명이인 구분 후 선택한 강사 고유 식별값 |
operatingStartDate | 예 | 운영 기간 시작일 |
operatingEndDate | 예 | 운영 기간 종료일 |
weekday | 예 | 반복 요일 |
periodName | 조건부 | 교시로 등록할 때 사용 |
startTime | 조건부 | 실제 시각 직접 입력 또는 교시 변환 결과 |
endTime | 조건부 | 실제 시각 직접 입력 또는 교시 변환 결과 |
subject | 예 | 과목 |
timeSource | 예 | period_template 또는 manual |
성공 응답
{
"data": {
"rowId": "row_001",
"status": "normal",
"errors": [],
"warnings": []
}
}
사용자 오류 상황
- 등록되지 않은 학교명은 오류로 표시하고 등록된 학교를 선택하도록 한다.
- 동명이인 강사는 자동 연결하지 않고 운영자가 강사를 선택할 때까지 오류로 유지한다.
- 교시와 실제 시작·종료 시각이 모두 없으면 저장 차단 오류로 처리한다.
- 종료 시각이 시작 시각보다 늦지 않으면 저장할 수 없다.
- 교시 템플릿 시간과 직접 입력한 시간이 다르면 어떤 시간을 사용할지 확인하는 경고를 표시한다. (FR-008)
POST /api/class-plans/imports/{importId}/confirm
목적: 미리보기에서 모든 저장 차단 오류를 해결한 엑셀 행을 수업 계획으로 한 번에 확정한다. (FR-008, FR-010, FR-012, UIR-006)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans/imports/{importId}/confirm
요청값
{
"confirm": true
}
성공 응답
{
"data": {
"savedClassPlanCount": 118,
"temporaryAssignmentCount": 118,
"sameSchoolOverlapExceptionCount": 1
}
}
사용자 오류 상황
- 오류 행이 하나라도 남아 있으면 전체 확정을 막고 오류 행을 다시 보여준다.
- 확정 시점에 다른 운영자의 저장으로 새 시간 중복이 생기면 전체 확정을 완료하지 않고 충돌한 행을 표시한다.
- 서로 다른 학교의 시간 중복은 저장할 수 없다.
- 같은 학교 시간 중복은 예외 사유가 있는 경우에만 저장할 수 있다. (FR-008, FR-010, FR-012, NFR-002)
6. 수업 계획 직접 등록과 수정
GET /api/class-plans
목적: 운영자가 학교별 또는 강사별 수업 계획을 조회한다. (FR-009, FR-013, UIR-005, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/class-plans
요청값
| 값 | 필수 | 설명 |
|---|---|---|
semesterId | 아니오 | 학기 필터 |
schoolId | 아니오 | 학교 필터 |
instructorId | 아니오 | 강사 필터 |
weekday | 아니오 | 요일 필터 |
publicationStatus | 아니오 | temporary, confirmed, published |
includeExceptions | 아니오 | 같은 학교 시간 겹침 예외 포함 여부 |
성공 응답
{
"data": {
"items": [
{
"classPlanId": "class_001",
"school": {
"schoolId": "school_001",
"schoolName": "새봄초등학교"
},
"semester": {
"semesterId": "semester_2026_2",
"semesterName": "2026년 2학기"
},
"operatingPeriod": {
"startDate": "2026-08-20",
"endDate": "2027-02-10"
},
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"instructor": {
"instructorId": "instructor_014",
"instructorName": "김미술"
},
"assignmentStatus": "temporary",
"sameSchoolOverlapException": null
}
]
}
}
사용자 오류 상황
- 필터 결과가 없으면 시간표 전체가 없다는 뜻으로 오해하지 않도록 현재 적용한 필터를 표시한다.
- 등록된 수업이 하나도 없으면 직접 등록과 엑셀 업로드 버튼을 보여준다. (FR-013, UIR-007)
POST /api/class-plans
목적: 운영자가 화면에서 수업 계획과 담당 강사를 한 건씩 직접 등록한다. (FR-009, FR-010, FR-012, UIR-005)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans
요청값
{
"schoolId": "school_001",
"semesterId": "semester_2026_2",
"operatingStartDate": "2026-08-20",
"operatingEndDate": "2027-02-10",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"timeSource": "period_template",
"subject": "미술",
"instructorId": "instructor_014",
"sameSchoolOverlapExceptionReason": null
}
성공 응답
{
"data": {
"classPlanId": "class_001",
"assignmentStatus": "temporary",
"overlapStatus": "none"
}
}
사용자 오류 상황
- 운영 기간이 학기 범위를 벗어나면 확인할 항목을 표시한다.
- 교시를 선택했지만 실제 시간을 찾을 수 없으면 교시 템플릿 등록 또는 실제 시각 직접 입력을 안내한다.
- 종료 시각이 시작 시각보다 늦지 않으면 저장하지 않는다.
- 다른 학교 수업과 시간이 겹치면 저장을 막고 충돌한 두 수업 비교 화면을 연다.
- 같은 학교 수업과 시간이 겹치면 예외 사유 입력 후에만 저장할 수 있다. (FR-009, FR-010, FR-012)
PATCH /api/class-plans/{classPlanId}
목적: 운영자가 기존 수업 계획의 학교, 운영 기간, 시간, 과목 또는 담당 강사를 수정한다. (FR-009, FR-010, FR-011, FR-012, FR-017, UIR-005, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: PATCH /api/class-plans/{classPlanId}
요청값
{
"weekday": "TUE",
"periodName": "4교시",
"startTime": "11:30",
"endTime": "12:10",
"subject": "미술",
"instructorId": "instructor_018",
"sameSchoolOverlapExceptionReason": null,
"expectedUpdatedAt": "2026-08-25T09:55:00+09:00"
}
성공 응답
{
"data": {
"classPlanId": "class_001",
"assignmentStatus": "temporary",
"publicationImpact": {
"wasPublished": true,
"changeNotificationRequired": true
}
}
}
사용자 오류 상황
- 저장 중 새 시간 중복이 발생하면 저장하지 않고 충돌 정보를 반환한다.
- 이미 공개된 수업의 변경은 다음 공개 처리 시 강사에게 변경 알림을 만들 수 있도록 변경 전후 값을 남긴다.
- 다른 사용자가 먼저 수정한 경우 최신 내용을 확인한 뒤 다시 저장하도록 안내한다. (FR-009, FR-010, FR-011, FR-017)
7. 시간 중복 검사와 현장 수정
POST /api/class-plans/overlap-check
목적: 수업 계획을 저장하거나 수정하기 전에 동일 강사의 시간 중복 여부를 확인한다. (FR-010, NFR-002, NFR-003)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans/overlap-check
요청값
{
"classPlanId": "class_001",
"schoolId": "school_001",
"instructorId": "instructor_014",
"operatingStartDate": "2026-08-20",
"operatingEndDate": "2027-02-10",
"weekday": "TUE",
"startTime": "10:40",
"endTime": "11:20"
}
classPlanId는 새 수업 등록 시 생략할 수 있다.
성공 응답: 중복 없음
{
"data": {
"hasOverlap": false,
"canSave": true
}
}
성공 응답: 다른 학교 시간 중복
{
"data": {
"hasOverlap": true,
"canSave": false,
"overlapType": "different_school",
"conflicts": [
{
"classPlanId": "class_099",
"schoolName": "푸른초등학교",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:50",
"endTime": "11:30",
"subject": "창의미술",
"instructorName": "김미술"
}
]
}
}
성공 응답: 같은 학교 시간 중복
{
"data": {
"hasOverlap": true,
"canSave": false,
"overlapType": "same_school",
"exceptionAvailable": true,
"conflicts": [
{
"classPlanId": "class_099",
"schoolName": "새봄초등학교",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:50",
"endTime": "11:30",
"subject": "창의미술",
"instructorName": "김미술"
}
]
}
}
사용자 오류 상황
14:00~14:40과14:40~15:20은 시간이 이어질 뿐 겹치지 않으므로 중복으로 처리하지 않는다.- 운영 기간이 겹치지 않으면 시간이 같아도 중복으로 처리하지 않는다.
- 이름이 같아도 강사 고유 식별값이 다르면 다른 강사로 처리한다.
- 이 확인 결과만으로 저장을 허용하지 않으며, 실제 저장 시 서버와 데이터베이스가 다시 검사한다. (FR-010, NFR-002)
POST /api/class-plans/{classPlanId}/resolve-overlap
목적: 서로 다른 학교의 시간 중복이 발견됐을 때, 충돌한 수업을 나란히 보고 한쪽 수업을 수정해 다시 저장한다. (FR-011, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans/{classPlanId}/resolve-overlap
요청값
{
"targetClassPlanId": "class_001",
"changes": {
"instructorId": "instructor_018",
"periodName": "4교시",
"startTime": "11:30",
"endTime": "12:10"
}
}
성공 응답
{
"data": {
"classPlanId": "class_001",
"saved": true,
"remainingConflicts": []
}
}
사용자 오류 상황
- 변경한 수업이 다른 수업과 새로 겹치면 새 충돌 수업을 보여주고 저장을 막는다.
- 수정을 취소하면 기존에 저장되지 않은 변경 내용은 버린다.
- 서로 다른 학교 시간 중복은 사유 입력만으로 강제 저장할 수 없다. (FR-011)
POST /api/class-plans/{classPlanId}/same-school-overlap-exception
목적: 같은 학교 안에서 의도적으로 시간이 겹치는 수업을 예외 사유와 함께 저장한다. (FR-012, FR-019, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/class-plans/{classPlanId}/same-school-overlap-exception
요청값
{
"conflictingClassPlanId": "class_099",
"reason": "학교 요청으로 두 반의 인수인계 시간이 10분 겹칩니다."
}
성공 응답
{
"data": {
"classPlanId": "class_001",
"exceptionId": "exception_001",
"saved": true,
"reason": "학교 요청으로 두 반의 인수인계 시간이 10분 겹칩니다.",
"createdAt": "2026-08-25T10:30:00+09:00"
}
}
사용자 오류 상황
- 예외 사유가 비어 있으면 저장할 수 없다.
- 두 수업이 서로 다른 학교에 속하면 예외 저장을 허용하지 않는다.
- 이후 학교, 운영 기간, 요일 또는 실제 시간이 바뀌면 예외가 여전히 가능한지 다시 검사한다. (FR-012)
8. 전체 시간표, 확정 시간표와 공개
GET /api/timetables
목적: 여러 학교의 수업 배정 상태, 시간 중복, 같은 학교 시간 겹침 예외를 전체 시간표에서 확인한다. (FR-013, FR-019, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/timetables
요청값
| 값 | 필수 | 설명 |
|---|---|---|
semesterId | 아니오 | 학기 필터 |
schoolId | 아니오 | 학교 필터 |
instructorId | 아니오 | 강사 필터 |
weekday | 아니오 | 요일 필터 |
status | 아니오 | 배정 또는 공개 상태 필터 |
성공 응답
{
"data": {
"items": [
{
"classPlanId": "class_001",
"schoolName": "새봄초등학교",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"instructorName": "김미술",
"assignmentStatus": "temporary",
"overlapStatus": "same_school_exception",
"exceptionId": "exception_001"
}
],
"summary": {
"unresolvedDifferentSchoolOverlapCount": 0,
"sameSchoolOverlapExceptionCount": 1
}
}
}
사용자 오류 상황
- 등록된 수업이 없으면 엑셀 업로드 또는 수업 직접 등록으로 이동할 수 있는 빈 화면을 보여준다.
- 필터 결과가 없으면 적용 중인 조건을 표시하고 필터를 초기화할 수 있게 한다. (FR-013)
POST /api/timetable-publications/validate
목적: 운영자가 시간표를 확정·공개하기 전에 선택한 범위에 해결되지 않은 시간 중복 또는 예외 사유 누락이 있는지 검사한다. (FR-014)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/timetable-publications/validate
요청값
{
"semesterId": "semester_2026_2",
"classPlanIds": ["class_001", "class_002"]
}
classPlanIds를 생략하면 선택한 학기의 전체 수업 계획을 검사한다.
성공 응답
{
"data": {
"canPublish": true,
"differentSchoolOverlapCount": 0,
"sameSchoolExceptionMissingReasonCount": 0,
"classPlanCount": 120
}
}
사용자 오류 상황
- 해결되지 않은 서로 다른 학교 시간 중복이 있으면 충돌한 수업 목록을 반환하고 공개를 막는다.
- 같은 학교 시간 중복에 예외 사유가 없으면 해당 수업을 반환하고 공개를 막는다. (FR-014)
POST /api/timetable-publications
목적: 충돌 검사를 통과한 수업 계획을 확정 시간표로 만들고 강사에게 공개한다. (FR-014, FR-016, FR-017, UIR-007, UIR-010)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/timetable-publications
요청값
{
"semesterId": "semester_2026_2",
"classPlanIds": ["class_001", "class_002"],
"publish": true
}
성공 응답
{
"data": {
"publicationId": "publication_001",
"semesterId": "semester_2026_2",
"publishedAt": "2026-08-25T11:00:00+09:00",
"publishedClassPlanCount": 120,
"affectedInstructorCount": 35,
"createdNotificationCount": 35
}
}
사용자 오류 상황
- 공개 전 검사에서 중복이 발견되면 확정 시간표와 공개본을 만들지 않는다.
- 공개 처리 중 일부 수업만 반영되는 오류가 나면 공개본 전체 생성을 취소한다.
- 공개된 시간표의 수정 내용을 다시 공개하면 영향을 받는 강사에게 변경 알림을 생성한다. (FR-014, FR-017)
9. 강사 초대와 계정 연결
POST /api/instructors/{instructorId}/invitations
목적: 운영자가 강사 계정 연결을 위한 초대 링크를 생성한다. (FR-015, UIR-003)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/instructors/{instructorId}/invitations
요청값
{
"expiresInDays": 7
}
expiresInDays의 기본값과 최대값은 확인 필요다.
성공 응답
{
"data": {
"invitationId": "invite_001",
"instructorId": "instructor_014",
"status": "invited",
"invitationUrl": "https://example.com/invitations/accept?token=...",
"expiresAt": "2026-09-01T23:59:59+09:00"
}
}
사용자 오류 상황
- 이미 다른 계정에 연결된 강사 정보에는 새 초대를 만들지 않는다.
- 운영자는 생성된 링크를 복사해 원하는 채널로 직접 전달한다.
- 카카오톡 자동 발송은 첫 개발 범위에 포함하지 않는다. (FR-015)
GET /api/invitations/{token}
목적: 강사가 초대 링크를 열었을 때 초대 상태와 연결할 위탁업체·강사 정보를 확인한다. (FR-015, UIR-008)
인증: 불필요
HTTP 방식과 경로: GET /api/invitations/{token}
요청값
없음
성공 응답
{
"data": {
"invitationStatus": "valid",
"agencyName": "방과후 운영 한눈에 교육",
"instructorName": "김미술",
"expiresAt": "2026-09-01T23:59:59+09:00"
}
}
사용자 오류 상황
- 유효하지 않거나 만료된 링크면 “초대 링크를 사용할 수 없습니다. 운영자에게 새 초대를 요청해 주세요.”라고 안내한다.
- 이미 연결을 마친 링크면 로그인 후 개인 시간표로 이동하도록 안내한다. (FR-015)
POST /api/invitations/{token}/accept
목적: 강사가 새 계정을 만들거나 기존 계정으로 로그인해 등록된 강사 정보와 연결한다. (FR-015, FR-002, UIR-008)
인증: 선택. 기존 계정 로그인 상태라면 필요, 신규 계정 생성 시 불필요
HTTP 방식과 경로: POST /api/invitations/{token}/accept
요청값: 신규 계정 생성
{
"mode": "sign_up",
"email": "instructor@example.com",
"password": "비밀번호"
}
요청값: 기존 계정 연결
{
"mode": "sign_in",
"email": "instructor@example.com",
"password": "비밀번호"
}
성공 응답
{
"data": {
"instructorId": "instructor_014",
"agencyId": "agency_001",
"connectionStatus": "connected",
"redirectPath": "/instructor/timetable"
}
}
사용자 오류 상황
- 이미 다른 계정과 연결된 강사 정보는 중복 연결하지 않는다.
- 초대 링크와 무관한 다른 강사 정보에 임의로 연결할 수 없다.
- 로그인 정보가 맞지 않으면 계정 존재 여부를 구분하지 않고 로그인 실패를 안내한다. (FR-015, FR-002)
10. 강사 개인 시간표
GET /api/instructor/timetable
목적: 강사가 자신에게 공개된 개인 시간표를 확인한다. (FR-016, UIR-009, UIR-010)
인증: 강사 필요
HTTP 방식과 경로: GET /api/instructor/timetable
요청값
| 값 | 필수 | 설명 |
|---|---|---|
semesterId | 아니오 | 학기 선택 |
weekday | 아니오 | 요일 필터 |
성공 응답
{
"data": {
"instructor": {
"instructorId": "instructor_014",
"instructorName": "김미술"
},
"items": [
{
"classPlanId": "class_001",
"schoolName": "새봄초등학교",
"semesterName": "2026년 2학기",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"publishedAt": "2026-08-25T11:00:00+09:00"
}
]
}
}
사용자 오류 상황
- 아직 강사 초대 연결이 끝나지 않은 경우 초대 연결 화면으로 이동한다.
- 공개된 시간표가 없으면 “운영자가 시간표를 공개하면 이곳에서 확인할 수 있습니다.”라고 보여준다.
- 다른 강사의 시간표 또는 전체 시간표는 조회할 수 없다. (FR-016, NFR-001)
11. 시간표 공개·변경 알림과 알림함
GET /api/instructor/notifications
목적: 강사가 시간표 공개·변경 알림을 모아 확인한다. (FR-017, FR-018, UIR-011)
인증: 강사 필요
HTTP 방식과 경로: GET /api/instructor/notifications
요청값
| 값 | 필수 | 설명 |
|---|---|---|
status | 아니오 | unread, read, all |
page | 아니오 | 페이지 번호 |
pageSize | 아니오 | 한 화면에 표시할 수 |
성공 응답
{
"data": {
"unreadCount": 2,
"items": [
{
"notificationId": "notification_001",
"type": "timetable_published",
"title": "2026년 2학기 시간표가 공개되었습니다.",
"message": "새봄초등학교 화요일 3교시 미술 수업을 확인해 주세요.",
"relatedClassPlanId": "class_001",
"createdAt": "2026-08-25T11:00:00+09:00",
"readAt": null
},
{
"notificationId": "notification_002",
"type": "timetable_changed",
"title": "시간표가 변경되었습니다.",
"message": "새봄초등학교 화요일 수업 시간이 10:40~11:20에서 11:30~12:10으로 변경되었습니다.",
"relatedClassPlanId": "class_001",
"createdAt": "2026-08-26T10:00:00+09:00",
"readAt": null
}
]
}
}
사용자 오류 상황
- 알림이 없으면 “새 알림이 없습니다.”라고 표시한다.
- 강사는 본인에게 생성된 알림만 볼 수 있다. (FR-018)
PATCH /api/instructor/notifications/{notificationId}
목적: 강사가 알림을 읽음으로 표시한다. (FR-018, UIR-011)
인증: 강사 필요
HTTP 방식과 경로: PATCH /api/instructor/notifications/{notificationId}
요청값
{
"read": true
}
성공 응답
{
"data": {
"notificationId": "notification_001",
"readAt": "2026-08-25T11:10:00+09:00"
}
}
사용자 오류 상황
- 다른 강사의 알림을 읽음 처리하려는 요청은 차단한다.
- 이미 읽은 알림은 같은 상태로 반환한다. (FR-018)
POST /api/timetable-publications/{publicationId}/notifications
목적: 시간표 공개 또는 공개된 시간표 변경 시 영향을 받는 강사의 변경 알림을 생성한다. 일반 사용자가 직접 실행하는 화면용 요청이 아니라, 운영자가 시간표를 공개할 때 연결해 사용한다. (FR-017, FR-014)
인증: 운영자 필요
HTTP 방식과 경로: POST /api/timetable-publications/{publicationId}/notifications
요청값
{
"notificationType": "timetable_published"
}
notificationType 값:
timetable_published: 처음 공개된 시간표 알림timetable_changed: 이미 공개된 수업의 변경 알림
성공 응답
{
"data": {
"publicationId": "publication_001",
"notificationType": "timetable_published",
"createdNotificationCount": 35
}
}
사용자 오류 상황
- 공개되지 않은 시간표에는 알림을 만들지 않는다.
- 같은 공개 처리에서 동일 강사에게 같은 알림이 중복 생성되지 않도록 한다.
- 알림 생성 실패 시 시간표 공개 상태와 알림 생성 상태를 운영자가 확인할 수 있어야 한다. 세부 재시도 방식은 확인 필요다. (FR-017)
12. 공개본과 예외 기록 조회
GET /api/timetable-publications
목적: 운영자가 학기별 시간표 공개 이력과 공개 시각을 조회한다. (FR-019, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/timetable-publications
요청값
| 값 | 필수 | 설명 |
|---|---|---|
semesterId | 아니오 | 학기 필터 |
page | 아니오 | 페이지 번호 |
pageSize | 아니오 | 한 화면에 표시할 수 |
성공 응답
{
"data": {
"items": [
{
"publicationId": "publication_001",
"semesterName": "2026년 2학기",
"publishedAt": "2026-08-25T11:00:00+09:00",
"publishedClassPlanCount": 120,
"publishedBy": {
"userId": "user_001",
"name": "홍길동"
}
}
]
}
}
사용자 오류 상황
- 공개 이력이 없으면 아직 공개하지 않은 시간표임을 명확히 표시한다.
- 다른 위탁업체의 공개 이력은 조회할 수 없다. (FR-019, NFR-001)
GET /api/timetable-publications/{publicationId}
목적: 운영자가 특정 확정 시간표의 수업 목록과 공개 당시 상태를 확인한다. (FR-019, FR-014)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/timetable-publications/{publicationId}
요청값
없음
성공 응답
{
"data": {
"publicationId": "publication_001",
"semesterName": "2026년 2학기",
"publishedAt": "2026-08-25T11:00:00+09:00",
"classPlans": [
{
"classPlanId": "class_001",
"schoolName": "새봄초등학교",
"weekday": "TUE",
"periodName": "3교시",
"startTime": "10:40",
"endTime": "11:20",
"subject": "미술",
"instructorName": "김미술"
}
]
}
}
사용자 오류 상황
- 존재하지 않는 공개본이면 공개 이력 목록으로 돌아가도록 안내한다. (FR-019)
GET /api/same-school-overlap-exceptions
목적: 운영자가 같은 학교 시간 겹침 예외 기록, 사유, 처리자와 처리 시각을 조회한다. (FR-019, FR-012, UIR-007)
인증: 운영자 필요
HTTP 방식과 경로: GET /api/same-school-overlap-exceptions
요청값
| 값 | 필수 | 설명 |
|---|---|---|
semesterId | 아니오 | 학기 필터 |
schoolId | 아니오 | 학교 필터 |
instructorId | 아니오 | 강사 필터 |
성공 응답
{
"data": {
"items": [
{
"exceptionId": "exception_001",
"schoolName": "새봄초등학교",
"instructorName": "김미술",
"classPlans": [
{
"classPlanId": "class_001",
"subject": "미술",
"weekday": "TUE",
"startTime": "10:40",
"endTime": "11:20"
},
{
"classPlanId": "class_099",
"subject": "창의미술",
"weekday": "TUE",
"startTime": "10:50",
"endTime": "11:30"
}
],
"reason": "학교 요청으로 두 반의 인수인계 시간이 10분 겹칩니다.",
"handledBy": "홍길동",
"createdAt": "2026-08-25T10:30:00+09:00"
}
]
}
}
사용자 오류 상황
- 조회 결과가 없으면 등록된 같은 학교 시간 겹침 예외가 없다고 표시한다.
- 서로 다른 학교 수업의 시간 중복 기록은 예외로 조회되지 않으며, 저장 자체가 차단된다. (FR-019, FR-010)
13. 비기능 구현 기준
위탁업체별 데이터 분리 (NFR-001)
- 학교, 강사, 학기, 교시 템플릿, 수업 계획, 확정 시간표, 시간표 공개, 변경 알림, 예외 기록은 모두 위탁업체 식별값에 연결한다.
- URL의 식별값만 바꿔 다른 위탁업체 정보를 보거나 수정할 수 없어야 한다.
- 서버는 요청마다 로그인 사용자와 위탁업체의 연결 관계를 확인한다.
중복 배정의 서버·데이터베이스 차단 (NFR-002)
- 화면의 사전 검사와 별도로 수업 계획 저장·수정·엑셀 확정·시간표 공개 시점에 다시 중복을 검사한다.
- 동시에 두 운영자가 수업 계획을 저장해 서로 다른 학교 시간 중복이 생기는 상황도 막아야 한다.
- 서로 다른 학교의 시간 중복은 예외 사유로 저장할 수 없다.
- 같은 학교 시간 중복은 예외 사유, 처리 운영자, 처리 시각, 관련 수업 계획을 모두 남긴 경우에만 저장할 수 있다.
시간 처리 일관성 (NFR-003)
- 실제 시작·종료 시각을 시간 중복의 유일한 판단 기준으로 사용한다.
- 시간 중복은 아래 조건을 만족할 때 발생한다.
기존 수업 시작 < 새 수업 종료
그리고
새 수업 시작 < 기존 수업 종료
- 종료 시각과 다음 수업 시작 시각이 같은 경우는 시간 중복이 아니다.
- 교시명은 화면 표시와 실제 시작·종료 시각 자동 입력을 위해 사용하지만, 중복 판단은 교시명 자체가 아닌 실제 시각으로 한다.
- 엑셀의 숫자형 시간과 문자열 시간을 같은
HH:mm형식으로 변환한다.
인증과 전송 보안 (NFR-004)
- 로그인, 초대 연결, 모든 운영자·강사 데이터 요청은 암호화된 연결을 사용한다.
- 비밀번호는 원문으로 저장하지 않는다.
- 로그인 실패 시 이메일 존재 여부를 드러내지 않는다.
- 초대 링크는 임의 추측이 어려운 값으로 만들고, 만료·사용 완료 상태를 확인한다.
민감정보 최소화 (NFR-005)
- 성범죄 경력·아동학대 전력 조회 회신서 등 민감한 원본 파일을 저장하지 않는다.
- 첫 개발 범위에는 서류 상태와 만료일 데이터도 포함하지 않는다.
- 강사 연락처와 이메일은 운영자 관리와 초대에 필요한 범위에서만 사용한다.
엑셀 업로드 안전성 (NFR-006)
- 지정 양식과 지원 파일 형식만 받는다.
- 파일을 읽는 동안 기존 수업 계획을 변경하지 않는다.
- 모든 행의 검증이 끝나고 운영자가 확정할 때만 수업 계획으로 저장한다.
- 읽지 못한 행, 필수값 누락, 등록되지 않은 학교, 동명이인 강사, 시간 오류를 행 번호와 함께 보여준다.
접근성과 오류 표현 (NFR-007)
- 오류를 색상만으로 표현하지 않고 행 번호, 입력 항목, 짧은 오류 문구를 함께 제공한다.
- 엑셀 업로드 오류 행은 표에서 바로 수정할 수 있어야 한다.
- 시간 중복 경고는 충돌한 두 수업의 학교, 요일, 교시, 실제 시간, 과목, 강사를 나란히 보여준다.
- 강사가 개인 시간표와 알림함을 쉽게 확인할 수 있도록 웹의 작은 화면에서도 주요 정보가 가려지지 않아야 한다.
검증 데이터 규모 (NFR-008)
첫 검증 기준은 강사 약 35명, 학교 약 20곳의 실제 운영 데이터다.
- 다수 학교와 강사가 있는 엑셀 시간표를 임시 읽기, 행별 검증, 수정, 확정까지 처리할 수 있어야 한다.
- 전체 시간표에서 학교·강사·요일·학기 기준 필터를 적용해 필요한 수업을 확인할 수 있어야 한다.
- 정확한 최대 등록 건수와 동시 사용자 기준은 개발 계약 전 확인 필요다.
웹 제공 기준 (NFR-009)
- 서비스는 Next.js 기반 웹으로 제공한다.
- 모바일 앱 스토어 등록과 별도 모바일 앱 빌드는 전제하지 않는다.
- 운영자는 PC 환경에서 엑셀 업로드, 대량 시간표 확인, 충돌 수정을 수행할 수 있어야 한다.
- 강사는 웹에서 개인 시간표와 알림함을 확인할 수 있어야 한다.