
Authenticate to Soongsil University LMS and fetch terms, courses, todos, attendance, announcements and scores; maintains session cookies/tokens, supports PEM normalization and progress callbacks.
숭실대학교 LMS(Canvas, LearningX), 유세인트(U-Saint) 및 숭실사이버대학교(KCU)의 학기, 강의, 할 일, 출석, 공지, 수강편람, 시간표, 성적, 채플, 등록금, 장학금, 졸업사정표 등의 정보를 조회하기 위한 Kotlin Multiplatform 라이브러리입니다.
이 README는 **Swift Package Manager(SPM)**를 이용한 iOS 연동과 Gradle을 이용한 Android/Kotlin 연동을 기준으로 작성되었습니다.
iosArm64
macosArm64
현재 소스 빌드에는 JavaScript target이 포함되어 있지 않습니다. iOS는 실제 기기(iosArm64)와 시뮬레이터(iosX64, iosSimulatorArm64)를 지원합니다. iOS 앱 연동은 GitHub Release에 배포된 XCFramework를 사용하는 SPM 방식을 권장합니다.
LmsApi 싱글톤과 로그인·조회 콜백 API는 그대로 유지됩니다.parse*, merge*, find*, fetchWebDynproHtml 같은 원본 응답 처리 함수는 내부 구현으로 변경되었습니다. 앱에서는 이 함수들을 직접 호출하지 말고 get* 조회 API를 사용해야 합니다.LmsApi는 프로세스 내에서 로그인 세션, 토큰, 쿠키와 일부 최신 조회 조건을 공유합니다. 새 로그인을 시작하면 이전 사용자의 세션과 캐시를 먼저 비우며, logout도 로그인 상태와 사용자별 캐시를 제거합니다.getTerms, getTodoList, getSubjects 등)는 외부에 콜백 API로 제공됩니다. U-Saint 조회 API는 Kotlin의 suspend 함수와 결과 콜백을 모두 제공합니다.internal 서비스로 분리되었지만 외부 사용법은 변경되지 않았습니다. 내부 구조와 기능 추가 규칙은 내부 구현 가이드를 참고하세요.iOS 앱 프로젝트에서는 Xcode의 Swift Package Manager를 통해 의존성을 추가할 수 있습니다.
File > Add Package Dependencies...를 선택합니다.https://github.com/chlwhdtn03/LMS-API
LmsApi를 선택하고 앱 target에 추가합니다.Swift 파일에서는 다음과 같이 import 합니다.
import LmsApi[!NOTE] SPM은 내부적으로 GitHub Release에 업로드된
LmsApi.xcframework.zip을 내려받아 사용하도록 구성되어 있습니다. SPM에서 설치되는 바이너리 버전은 루트Package.swift의 Release URL을 따르며, Maven 배포 버전과 별도로 갱신될 수 있습니다.
Android 또는 Kotlin Multiplatform(KMP) 프로젝트에서는 Gradle 의존성으로 추가하여 사용할 수 있습니다.
Android 단일 프로젝트 (build.gradle.kts):
dependencies {
implementation("io.github.chlwhdtn03:lms:1.6.13")
}Kotlin Multiplatform 프로젝트 (commonMain 의존성):
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.chlwhdtn03:lms:1.6.13")
}
}
}LMS 조회 기능과 대부분의 유세인트(U-Saint) 조회 기능은 LMS 로그인 완료 후 생성된 세션을 공유하여 호출할 수 있습니다. 로그인에 성공하면 학번 정보와 토큰 정보가 내부적으로 캐싱되어 이후 호출되는 API에 자동으로 적용됩니다. 공개 수강편람 조회는 예외로, 로그인 없이 사용할 수 있습니다.
[!NOTE] 숭실사이버대학교(KCU)는 별도 도메인(
portal.kcu.ac,lms.kcu.ac)과 세션을 사용하므로,LmsApi와 무관하게 CyberApi를 통해 독립적으로 로그인하고 조회합니다.
import LmsApi
func performLogin() {
LmsApi.shared.loginLMS(id: "학번", password: "비밀번호") { result in
if result.success {
print("로그인 성공")
} else {
print("로그인 실패: \(result.errorMessage ?? "알 수 없는 오류")")
}
}
}import io.github.chlwhdtn03.LmsApi
fun performLogin() {
LmsApi.loginLMS(id = "학번", password = "비밀번호") { result ->
if (result.success) {
println("로그인 성공")
} else {
println("로그인 실패: ${result.errorMessage}")
}
}
}유세인트 조회 기능은 시간표, 성적, 채플, 등록금, 장학금, 졸업사정표 데이터를 비동기식으로 파싱하여 반환합니다. Kotlin에서는 suspend 함수 또는 결과 콜백을 사용할 수 있으며, iOS에서는 Kotlin/Native가 변환한 비동기 API를 Swift async/await 또는 결과 콜백 형태로 사용할 수 있습니다.
import LmsApi
func loadUSaintInformation() {
Task {
do {
// 1. 시간표 조회
let timetable = try await LmsApi.shared.getTimetable()
print("시간표 학기: \(timetable.year) \(timetable.semester)")
for item in timetable.items {
print("- [\(item.subject)] \(item.classroom) / \(item.professor)")
}
// 2. 성적 상세 조회 (year, semester에 nil을 전달하면 캐싱된 최근 학기 성적을 조회합니다.)
let gradeTable = try await LmsApi.shared.getGradeTable(year: nil, semester: nil)
for grade in gradeTable.items {
print("- \(grade.subjectName): \(grade.grade) (\(grade.credits)학점)")
}
// 3. 성적 요약 조회 (학기별 신청학점, 평점평균, 석차 등)
let gradeSummary = try await LmsApi.shared.getSemesterGradeSummaryTable()
for summary in gradeSummary.items {
print("- \(summary.year)년 \(summary.semester?.name ?? "")학기 평점: \(summary.gpa)")
}
// 4. 채플 출결 및 좌석 조회 (year, semester에 nil을 전달하면 캐싱된 최근 채플 내역을 조회합니다.)
let chapel = try await LmsApi.shared.getChapelTable(year: nil, semester: nil)
print("배정 좌석 번호: \(chapel.seatStatusTable.items.first?.seatNo ?? "없음")")
// 5. 등록금 납부 내역 조회
let tuition = try await LmsApi.shared.getTuitionTable()
for record in tuition.items {
print("- \(record.year) \(record.semester) 납부 금액: \(record.paymentAmount)")
}
// 6. 장학 수혜 내역 조회
let scholarship = try await LmsApi.shared.getScholarshipHistoryTable()
for item in scholarship.items {
print("- \(item.year)학년도 \(item.semester)학기 [\(item.scholarshipName)] 수혜 금액: \(item.actualAmount)")
}
// 7. 졸업사정표 조회
let graduate = try await LmsApi.shared.getGraduateTable()
for row in graduate.items {
print("- \(row.classification) (졸업요건: \(row.standardValue)학점 / 취득: \(row.calculatedValue)학점)")
}
} catch {
print("유세인트 정보 조회 실패: \(error.localizedDescription)")
}
}
}import io.github.chlwhdtn03.LmsApi
import io.github.chlwhdtn03.data.Lms.Semester
fun loadUSaintForAndroid() {
// 1. 시간표 조회
LmsApi.getTimetable { result ->
if (result.success && result.timetable != null) {
println("시간표 학기: ${result.timetable.year} ${result.timetable.semester}")
}
}
// 2. 성적 상세 조회
LmsApi.getGradeTable(year = "2026", semester = Semester.FIRST) { result ->
if (result.success && result.gradeTable != null) {
result.gradeTable.items.forEach { cell ->
println("${cell.subjectName}: ${cell.grade}")
}
}
}
// 3. 성적 요약 조회
LmsApi.getSemesterGradeSummaryTable { result ->
if (result.success && result.summaryTable != null) {
result.summaryTable.items.forEach { summary ->
println("${summary.year}학년도 평점평균: ${summary.gpa}")
}
}
}
// 4. 채플 조회
LmsApi.getChapelTable(year = null, semester = null) { result ->
if (result.success && result.chapelInformation != null) {
println("결석 횟수: ${result.chapelInformation.seatStatusTable.items.firstOrNull()?.absenceCount}")
}
}
// 5. 등록금 납부 내역 조회
LmsApi.getTuitionTable { result ->
if (result.success && result.tuitionTable != null) {
println("마지막 납부액: ${result.tuitionTable.items.firstOrNull()?.paymentAmount}")
}
}
// 6. 장학 수혜 내역 조회
LmsApi.getScholarshipHistoryTable { result ->
if (result.success && result.scholarshipHistoryTable != null) {
println("수혜 장학금명: ${result.scholarshipHistoryTable.items.firstOrNull()?.scholarshipName}")
}
}
// 7. 졸업사정표 조회
LmsApi.getGraduateTable { result ->
if (result.success && result.graduateTable != null) {
println("이수결과: ${result.graduateTable.items.firstOrNull()?.result}")
}
}
}수강편람은 조회 유형별 콤보박스가 서로 연동되므로 먼저 옵션을 받고, 선택한 key를
filterKeys에 같은 순서로 추가합니다. 검색 결과는 서버의 500줄 단위 페이지를 모두 합쳐
반환합니다.
val baseQuery = CourseCatalogQuery(
year = "2026",
semester = Semester.FIRST,
category = CourseCatalogCategory.DEPARTMENT,
)
val collegeOptions = LmsApi.getCourseCatalogSearchOptions(baseQuery)
val collegeKey = collegeOptions.filters[0].options.first().key
val departmentOptions = LmsApi.getCourseCatalogSearchOptions(
baseQuery.copy(filterKeys = listOf(collegeKey)),
)
val departmentKey = departmentOptions.filters[1].options.first().key
val courses = LmsApi.getCourseCatalogTable(
baseQuery.copy(filterKeys = listOf(collegeKey, departmentKey)),
)
// 목록 조회 단계에서는 OZ Viewer를 실행하지 않습니다.
// 사용자가 특정 강좌의 계획 버튼을 누른 시점에만 PDF를 만듭니다.
val selectedCourse = courses.items.first()
val pdfBytes: ByteArray = selectedCourse.loadPlan()
// 예비수강신청도 로그인 후 조회한 과목에서 동일하게 사용합니다.
val preRegistration = LmsApi.getPreRegistrationTable()
val preRegistrationPdf: ByteArray = preRegistration.items.first().loadPlan()CourseCatalogCategory.SUBJECT와 PROFESSOR는 keyword를 사용합니다. 버튼형 계획서는
재사용할 수 있는 링크가 아니므로 목록의 plan은 빈 문자열일 수 있습니다. Android와
iOS에서는 과목의 loadPlan()을 호출하면 내부에서 일회성 OZ Viewer를 새로 열고 PDF가
완성된 뒤 바이트를 반환합니다. 이 작업은 네트워크 요청과 보고서 렌더링을 포함하므로 UI
로딩 상태를 표시하는 것이 좋습니다. PDF 크기와 생성 메모리를 줄이기 위해 폰트 임베딩은
비활성화되어 있으므로, 특수 폰트가 사용된 계획서는 플랫폼의 대체 폰트로 표시될 수 있습니다.
Android에서는 OZ 콜백의 전체 문자열을 Kotlin으로 넘기지 않고 WebView의 ArrayBuffer를
ByteArray로 직접 수신합니다. JVM과 macOS에서는 PDF 로드를 지원하지 않습니다.
iOS XCFramework에서는 loadPlan(completionHandler:)로 노출되며 결과 형식은
KotlinByteArray입니다. Swift의 Data로 변환하면 Apple 기본 프레임워크인
PDFKit의 PDFDocument(data:)와
PDFView로 앱 내부 네이티브 PDF 뷰어를 구성할 수 있습니다. 별도의 외부 PDF 뷰어
라이브러리는 필요하지 않습니다.
import LmsApi
import PDFKit
extension KotlinByteArray {
func toData() -> Data {
Data((0..<Int(size)).map { index in
UInt8(bitPattern: get(index: Int32(index)))
})
}
}
selectedCourse.loadPlan { bytes, error in
guard let bytes else {
print(error?.localizedDescription ?? "강의계획서 로드 실패")
return
}
let data = bytes.toData()
guard let document = PDFDocument(data: data) else {
print("올바른 PDF 데이터가 아닙니다.")
return
}
DispatchQueue.main.async {
pdfView.document = document
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
}
}SwiftUI에서는 PDFView를 UIViewRepresentable로 감싸서 사용할 수 있습니다.
import SwiftUI
import PDFKit
struct NativePDFView: UIViewRepresentable {
let data: Data
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.autoScales = true
view.displayMode = .singlePageContinuous
view.displayDirection = .vertical
view.document = PDFDocument(data: data)
return view
}
func updateUIView(_ view: PDFView, context: Context) {
view.document = PDFDocument(data: data)
}
}KotlinByteArray.toData()는 Swift에서 각 바이트를 읽어 새 Data를 만드는 과정이므로 PDF가
큰 경우 추가 복사 비용이 발생합니다. 변환 중에도 로딩 상태를 유지하고, 같은 PDF는 변환한
Data 또는 임시 파일을 재사용하는 것이 좋습니다. 추후 iOS 전용 API가 NSData를 직접
반환하도록 추가된다면 이 변환 단계는 생략할 수 있지만, 현재 공개 API의 반환 타입은
KotlinByteArray입니다.
LMS 조회 기능은 학기 목록, 강의 목록, 할 일(과제 및 동영상 시청 기한), 출석, 공지, 제출 및 점수 정보를 조회할 수 있는 기능을 제공합니다.
import LmsApi
func loadLMSTodos() {
LmsApi.shared.getTerms { termsResult in
guard termsResult.success else {
print("학기 조회 실패: \(termsResult.errorMessage ?? "알 수 없는 오류")")
return
}
guard let latestTerm = termsResult.terms.last else {
print("조회 가능한 학기가 없습니다.")
return
}
LmsApi.shared.getTodoList(
term: latestTerm,
loadingState: { progress in
print("진행률: \(Int(progress.floatValue * 100))%")
},
postHogDistinctId: nil
) { subjectsResult in
guard subjectsResult.success else {
print("할 일 조회 실패: \(subjectsResult.errorMessage ?? "알 수 없는 오류")")
return
}
for subject in subjectsResult.subjects {
print("과목: \(subject.name)")
for todo in subject.todoList {
print("- \(todo.title) (마감일: \(todo.due_date))")
}
}
}
}
}import io.github.chlwhdtn03.LmsApi
fun loadLMSForAndroid() {
LmsApi.getTerms { termsResult ->
if (termsResult.success) {
val latestTerm = termsResult.terms.lastOrNull() ?: return@getTerms
LmsApi.getTodoList(
term = latestTerm,
loadingState = { progress ->
println("loading: ${(progress * 100).toInt()}%")
},
completion = { subjectsResult ->
if (subjectsResult.success) {
subjectsResult.subjects.forEach { subject ->
subject.todoList.forEach { todo ->
println("[${subject.name}] ${todo.title} / ${todo.due_date}")
}
}
}
}
)
}
}
}숭실사이버대학교 포털(portal.kcu.ac) 및 LMS(lms.kcu.ac)에 로그인하여 수강 과목 목록과 주차별 강의 수강일람(출석 현황, 진도율, 학습 시간, 완료 여부 등)을 조회할 수 있습니다.
CyberApi는 숭실대학교 LMS 및 유세인트(LmsApi)와 별개의 도메인과 시스템을 사용하므로, 세션 및 쿠키를 공유하지 않는 독립적인 싱글톤 객체입니다.
iOS(Swift)에서는 Kotlin object CyberApi가 CyberApi.shared로 접근되며, async/await 또는 결과 콜백 방식을 모두 지원합니다.
import LmsApi
func loadCyberUniversityLectures() {
Task {
do {
// 1. 사이버대학교 포털 로그인
let loginSuccess = try await CyberApi.shared.login(id: "아이디", password: "비밀번호")
guard loginSuccess.boolValue else {
print("로그인 실패")
return
}
// 2. 이번 학기 수강과목 목록 조회
let subjects = try await CyberApi.shared.getSubjects()
for subject in subjects {
print("과목: \(subject.name) (\(subject.credit)학점) - 진도율: \(subject.progressPercent)%")
}
// 3. 첫 번째 과목의 주차별 수강일람 조회
if let firstSubject = subjects.first {
let weeks = try await CyberApi.shared.getWeeklyLectures(subject: firstSubject)
for week in weeks {
print("[\(week.weekNo)주차] \(week.topic) - 출석상태: \(week.attendanceStatus) (기간: \(week.attendancePeriod))")
for lecture in week.lectures {
print(" - \(lecture.lectureNo)강: \(lecture.statusText), 진도율: \(lecture.progressPercent)%, 학습시간: \(lecture.studyTime)/\(lecture.baseTime), 완료여부: \(lecture.isCompleted)")
}
}
}
// 4. 로그아웃 (세션 정리)
try await CyberApi.shared.logout()
} catch {
print("사이버대학교 정보 조회 실패: \(error.localizedDescription)")
}
}
}import LmsApi
func loadCyberWithCallback() {
CyberApi.shared.login(id: "아이디", password: "비밀번호") { loginResult in
guard loginResult.success else {
print("로그인 실패: \(loginResult.errorMessage ?? "알 수 없는 오류")")
return
}
CyberApi.shared.getSubjects { subjectsResult in
guard subjectsResult.success else {
print("수강과목 조회 실패: \(subjectsResult.errorMessage ?? "알 수 없는 오류")")
return
}
if let firstSubject = subjectsResult.subjects.first {
CyberApi.shared.getWeeklyLectures(subject: firstSubject) { lecturesResult in
if lecturesResult.success {
print("주차 수: \(lecturesResult.weeks.count)")
}
}
}
}
}
}Android/Kotlin에서는 코루틴(suspend) 또는 결과 콜백 오버로드를 모두 지원합니다.
import io.github.chlwhdtn03.CyberApi
suspend fun loadCyberUniversityForAndroid() {
try {
// 1. 사이버대학교 포털 로그인
val loginSuccess = CyberApi.login(id = "아이디", password = "비밀번호")
if (!loginSuccess) {
println("로그인 실패")
return
}
// 2. 이번 학기 수강과목 목록 조회
val subjects = CyberApi.getSubjects()
subjects.forEach { subject ->
println("[${subject.name}] 교수: ${subject.professor}, 학점: ${subject.credit}, 진도율: ${subject.progressPercent}%")
}
// 3. 특정 과목의 주차별 수강일람 조회
val firstSubject = subjects.firstOrNull() ?: return
val weeks = CyberApi.getWeeklyLectures(firstSubject)
weeks.forEach { week ->
println("${week.weekNo}주차: ${week.topic} (${week.attendancePeriod}) [${week.attendanceStatus}]")
week.lectures.forEach { lecture ->
println(" - ${lecture.lectureNo}강: ${lecture.statusText}, 진도율: ${lecture.progressPercent}%, 완료여부: ${lecture.isCompleted}")
}
}
// 4. 로그아웃
CyberApi.logout()
} catch (e: Exception) {
println("사이버대학교 조회 중 오류 발생: ${e.message}")
}
}import io.github.chlwhdtn03.CyberApi
fun loadCyberWithCallback() {
CyberApi.login(id = "아이디", password = "비밀번호") { loginResult ->
if (loginResult.success) {
CyberApi.getSubjects { subjectsResult ->
if (subjectsResult.success) {
val firstSubject = subjectsResult.subjects.firstOrNull() ?: return@getSubjects
CyberApi.getWeeklyLectures(firstSubject) { lecturesResult ->
if (lecturesResult.success) {
println("주차 수: ${lecturesResult.weeks.size}")
}
}
}
}
} else {
println("로그인 실패: ${loginResult.errorMessage}")
}
}
}fun loginLMS(id: String, password: String, completion: (LmsLoginResult) -> Unit)LMS 아이디와 비밀번호로 로그인합니다. 새 로그인 시도 전에 기존 세션을 제거하며, 로그인 후 사용자 정보가 실제로 조회된 경우에만 성공 처리합니다. 성공 시 유세인트 세션도 자동으로 공유됩니다.
로그인 여부는 외부에서 읽기만 가능한 LmsApi.isLoggined로 확인할 수 있습니다. 기존 호환성을 위해 현재 철자를 유지합니다.
fun logout(completion: () -> Unit)현재 로그인 상태와 쿠키, 사용자별 성적·채플 최신 조회 조건 캐시를 제거합니다.
fun getTerms(completion: (LmsTermsResult) -> Unit)로그인된 사용자의 학기 목록을 가져옵니다.
fun getTodoList(term: Term, loadingState: (Float) -> Unit = {}, completion: (LmsSubjectsResult) -> Unit)
fun getTodoList(
term: Term,
loadingState: (Float) -> Unit = {},
postHogDistinctId: String?,
completion: (LmsSubjectsResult) -> Unit,
)과목 기본 정보와 할 일 목록(과제, 동영상 등), 제출 정보를 빠르게 파싱하여 가져옵니다.
postHogDistinctId가 없는 오버로드를 사용하거나 null을 전달하면 분석 데이터를 전송하지 않습니다. 식별자를 전달하면 식별자별 하루 한 번 전송 여부를 20% 비율로 샘플링하며, 선택된 경우 Todo 동기화 통계와 항목 상태 스냅샷을 PostHog로 전송합니다. 이를 의도한 앱에서만 사용하세요.
fun getSubjects(term: Term, loadingState: (Float) -> Unit = {}, completion: (LmsSubjectsResult) -> Unit)과목 기본 정보, 할 일, 출석, 공지, 제출 및 점수 데이터를 모두 가져옵니다. 여러 API를 내부적으로 호출하므로 getTodoList에 비해 무겁습니다.
fun getLoginInfo(completion: (LmsLoginInfoResult) -> Unit)로그인한 학생의 이름, 학과, 로그인 ID, 이메일 등의 기본 신원 정보를 가져옵니다.
fun getCookies(completion: (LmsCookiesResult) -> Unit)현재 로그인된 세션의 쿠키 목록을 반환합니다. 외부 서비스와의 연동 시 사용합니다.
// 창업지원단 공지사항 조회
fun loadStartUpNotices(pageNum: Int = 1, completion: (StartUpNoticesResult) -> Unit)
// 장학 공지사항 조회
fun loadScholarships(pageNum: Int = 1, completion: (ScholarshipNoticesResult) -> Unit)suspend fun getTimetable(): Timetable
suspend fun getTimetable(year: String?, semester: Semester?): Timetable
fun getTimetable(completion: (LmsTimetableResult) -> Unit)
fun getTimetable(year: String?, semester: Semester?, completion: (LmsTimetableResult) -> Unit)개인의 유세인트 시간표를 조회합니다. 학년도와 학기를 생략하면 유세인트가 제공하는 기본 조회 기간을 사용합니다.
suspend fun getGradeTable(year: String? = null, semester: Semester? = null): GradeTable
fun getGradeTable(completion: (LmsGradeResult) -> Unit)
fun getGradeTable(year: String?, semester: Semester?, completion: (LmsGradeResult) -> Unit)지정된 학년도 및 학기의 성적 상세 내역을 조회합니다. 파라미터가 모두 null일 경우 캐싱된 최신 학기 성적을 반환합니다.
suspend fun getSemesterGradeSummaryTable(): SemesterGradeSummaryTable
fun getSemesterGradeSummaryTable(completion: (LmsSemesterGradeSummaryResult) -> Unit)전체 학기별 신청학점, 취득학점, 평점평균 및 석차 정보가 담긴 성적 요약 데이터를 조회합니다.
suspend fun getChapelTable(year: String? = null, semester: Semester? = null): ChapelInformation
fun getChapelTable(completion: (LmsChapelResult) -> Unit)
fun getChapelTable(year: String?, semester: Semester?, completion: (LmsChapelResult) -> Unit)지정된 학년도 및 학기의 채플 정보(좌석 번호, 주차별 출결, 결석계 신청 현황)를 조회합니다. (계절학기 조회 불가)
suspend fun getTuitionTable(): TuitionTable
fun getTuitionTable(completion: (LmsTuitionResult) -> Unit)학년도/학기별 등록금 고지액, 장학 감면액, 실납부일자 및 납부 금액 등 등록금 납부 이력을 조회합니다.
suspend fun getScholarshipHistoryTable(): ScholarshipHistoryTable
fun getScholarshipHistoryTable(completion: (LmsScholarshipHistoryResult) -> Unit)학기별 수혜한 장학금 명칭, 지급 방법, 선발 금액 및 실수혜금액 등의 내역을 조회합니다.
suspend fun getGraduateTable(): GraduateTable
fun getGraduateTable(completion: (LmsGraduateTableResult) -> Unit)졸업사정표 상의 이수구분별 졸업 기준 요건 학점, 본인 취득학점, 차이값 및 판정 결과를 조회합니다.
숭실사이버대학교 전용 API로, LmsApi와는 별도의 독립적인 싱글톤 객체입니다.
suspend fun login(id: String, password: String): Boolean
fun login(id: String, password: String, completion: (CyberLoginResult) -> Unit)사이버대학교 포털(portal.kcu.ac)에 로그인합니다. 내부적으로 RSA 공개키를 조회하여 비밀번호를 암호화한 뒤 로그인을 수행하며, 성공 시 세션 쿠키를 보관합니다.
로그인 여부는 외부에서 읽기 가능한 CyberApi.isLoggined 프로퍼티로 확인할 수 있습니다.
suspend fun logout()
fun logout(completion: () -> Unit)현재 사이버대학교 로그인 세션을 종료하고 로그인 상태를 해제합니다.
suspend fun getSubjects(): List<CyberSubject>
suspend fun getSubjects(postHogDistinctId: String?): List<CyberSubject>
fun getSubjects(completion: (CyberSubjectsResult) -> Unit)
fun getSubjects(postHogDistinctId: String?, completion: (CyberSubjectsResult) -> Unit)로그인된 사용자의 이번 학기 수강과목 목록(CyberSubject)을 가져옵니다. 과목명, 이수구분, 담당 교수, 학점, 전체 진도율 등이 포함되어 있습니다. (로그인 필수)
postHogDistinctId를 전달하면 전체 과목의 주차별 강의와 학습평가를 추가 조회한 뒤 todo_snapshot_cyber와 $identify를 함께 전송합니다. LMS와 동일하게 식별자별 UTC 날짜 기준 하루 한 번 전송 여부를 20% 확률로 샘플링하며, 전송 이력은 LMS와 별개인 메모리 캐시입니다. 식별자를 생략하거나 null/빈 문자열을 전달하면 추가 조회와 전송이 없습니다. 기존 앱에서 분석을 활성화하려면 CyberApi.getSubjects(postHogDistinctId = distinctId)를 호출하세요.
스냅샷 속성은 LMS와 동일하며, 동영상은 진도율 100% 이상이면 완료, 퀴즈/과제는 제출·응시 완료 상태이면 완료로 계산합니다. 미제출은 미완료이면서 마감이 지난 항목이고, 전체 개수에는 기한이 남은 항목도 포함됩니다. 시험/토론은 제외하며 본제출/재제출 행은 중복 집계하지 않습니다. 사이버 날짜는 한국 시간으로 해석하고 출석인정기간은 종료일 끝까지 인정합니다. 과목 코드는 문자열이므로 items.course_id도 문자열입니다.
$identify는 LMS와 사이버가 공유하는 PostHog 기본 식별 이벤트이며 $set/$set_once로 사용자 속성을 갱신합니다. 속성명은 LMS와 동일하게 유지하며 _cyber는 스냅샷 이벤트명에만 붙입니다. 동일한 식별자를 전달하면 최초/마지막 동기화 사용자 속성은 LMS와 사이버가 공유하므로, 서비스별 미제출률은 각 스냅샷 이벤트의 snapshot_unsubmitted_ratio로 분석하세요. 전체 과목 조회가 실패하면 스냅샷을 전송하지 않으며 조회 오류를 반환합니다. PostHog 전송 실패는 LMS와 동일하게 조회 결과에 영향을 주지 않습니다.
suspend fun getWeeklyLectures(subject: CyberSubject): List<CyberWeek>
fun getWeeklyLectures(subject: CyberSubject, completion: (CyberWeeklyLecturesResult) -> Unit)특정 수강과목의 주차별 수강일람(CyberWeek) 및 하위 강의 목록(CyberLecture)을 조회합니다. 각 주차별 출석인정기간, 출석 상태, 강의별 학습 시간, 진도율, 완료 여부(isCompleted)를 확인할 수 있습니다. (로그인 필수)
FIRST: 1학기 (코드: "090")SUMMER: 여름학기 (코드: "091")SECOND: 2학기 (코드: "092")WINTER: 겨울학기 (코드: "093")year: 학년도 (예: "2026학년도")semester: 학기 (예: "1학기")items: TimetableCell 리스트
dayOfWeek: 요일 (DayOfWeek Enum)period: 교시 (예: "1 교시")periodTime: 교시 시간 범위 (예: "(08:00-08:50)")subject: 과목명professor: 교수명time: 강의 시간 문자열classroom: 강의실year: 학년도semester: 학기items: GradeCell 리스트
subjectCode: 과목코드subjectName: 과목명classification: 이수구분 (예: "전공기초")credits: 학점 (예: "3.0")grade: 등급 (예: "A+")gradePoint: 평점 (예: "4.5")professor: 교수명items: SemesterGradeSummaryCell 리스트
year: 학년도semester: 학기attemptedCredits: 신청학점earnedCredits: 취득학점pfCredits: P/F학점gpa: 평점평균gpaSum: 평점계arithmeticMean: 산술평균semesterRank: 학기석차totalRank: 전체석차academicWarning: 학사경고여부consultationStatus: 상담여부failedYearStatus: 유급여부year: 학년도semester: 학기seatStatusTable: 좌석 정보 테이블 (classGroup, timetable, classroom, seatNo, absenceCount, gradeResult)attendanceTable: 출결 현황 테이블 (classGroup, date, lectureType, status)absenceTable: 결석계 신청 이력 테이블 (year, semester, detail)items: TuitionCell 리스트
year, semester: 학년도 및 학기grade: 학년(기)registrationType: 등록구분 (예: "정규등록")registrationDate: 납부일자amount: 고지금액reduction: 장학 감면액paymentAmount: 최종 실납부금액items: ScholarshipHistoryCell 리스트
year, semester: 장학금 지급 학년도 및 학기scholarshipName: 장학금명paymentMethod: 지급방법 (예: "고지서 감면")processStatus: 처리 상태selectedAmount: 선발금액actualAmount: 실수혜금액redeemedAmount: 환수금액replacedAmount: 교체금액replacedScholarshipName: 교체장학금명workDepartment: 근로부서items: GraduateTableCell 리스트
classification: 이수구분 (예: "전공선택")requirement: 졸업요건standardValue: 기준학점calculatedValue: 취득학점difference: 차이값result: 이수 여부 판정 (예: "합격", "미필")usedSubjects: 해당 요건에 사용된 과목명 리스트 (과목사용 열을 쉼표 단위로 분리)id: 학기 고유 IDname: 학기명start_at: 시작 시각end_at: 종료 시각id: 과목 고유 IDtermId: 학기 IDtermName: 학기명name: 과목명professor: 담당 교수totalStudents: 수강 인원todoList: 과제 및 할 일 목록attendances: 출석 기록 리스트discussions: 공지사항 목록submissions: 과제 제출 정보scoredAssignments: 평가 및 획득한 점수 리스트component_type: 항목 타입 (예: assignment, commons)assignment_id: 과제 고유 IDtitle: 제목due_date: 마감 기한assignment_id: 과제 고유 IDattachments: 첨부파일 목록attempt: 제출 시도 횟수cached_due_date: 마감 시각late: 지각 여부preview_url: 제출 파일 미리보기 URLsubmitted_at: 제출 시각submission_type: 제출 유형workflow_state: 제출 상태 (예: submitted, graded, unsubmitted)score: 획득 점수name: 과목명category: 이수구분 (예: "전공기초")professor: 담당 교수credit: 학점 (예: "3.0")progressPercent: 전체 진도율 (%)year: 학년도semesterCode: 학기 코드courseCode: 과목 코드deptCode: 학과 코드userNo: 사용자 번호weekNo: 주차 번호 (예: 1)attendancePeriod: 출석인정기간 (예: "2026.09.01 ~ 2026.09.14")topic: 주차 주제 / 강의명attendanceStatus: 출석 인정 상태 (예: "출석", "결석")lectures: 주차 내 개별 강의 리스트 (List<CyberLecture>)lectureNo: 강 번호 (예: 1)statusText: 학습 상태 텍스트 (예: "학습완료", "미학습")progressPercent: 진도율 (%)studyTime: 학습 시간 (예: "35분 20초")baseTime: 인정 기준 시간 (예: "30분 00초")videoFilePath: 동영상 파일 경로 (nullable)audioFilePath: 오디오 파일 경로 (nullable)isCompleted: 학습 완료 여부 계산 프로퍼티 (progressPercent >= 100)파서와 공개 범위 등 외부 서버가 필요 없는 테스트는 일반 JVM 테스트로 실행합니다.
./gradlew :library:jvmTest실제 LMS/U-Saint 네트워크 API와 각 콜백 오버로드를 함께 검증하려면 테스트 계정을 환경 변수로 전달합니다. 계정 정보가 없으면 통합 테스트는 외부 서버를 호출하지 않고 종료됩니다.
LMS_TEST_ID="학번" \
LMS_TEST_PASSWORD="비밀번호" \
./gradlew :library:jvmTest \
--tests "io.github.chlwhdtn03.LmsApiFullIntegrationTest"필요하면 LMS_TEST_TERM_ID, LMS_TEST_YEAR, LMS_TEST_SEMESTER도 지정할 수 있습니다. LMS_TEST_SEMESTER는 FIRST, SECOND, 090, 092, 1학기, 2학기 형식을 지원합니다. 실제 계정 정보는 소스 코드나 커밋에 저장하지 마세요.
사이버대학교(KCU) 계정으로 실제 서버와의 로그인 및 수강과목/수강일람 조회를 검증하려면 아래 환경 변수를 전달합니다:
CYBER_TEST_ID="사이버대학_아이디" \
CYBER_TEST_PASSWORD="사이버대학_비밀번호" \
./gradlew :library:jvmTest \
--tests "io.github.chlwhdtn03.CyberApiFullIntegrationTest" -i이 저장소의 루트에 있는 Package.swift는 Kotlin/Native 컴파일 결과물인 XCFramework를 바이너리 타겟으로 래핑하여 배포하도록 설정되어 있습니다.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "LmsApi",
platforms: [
.iOS(.v14),
],
products: [
.library(name: "LmsApi", targets: ["LmsApi"])
],
targets: [
.binaryTarget(
name: "LmsApi",
url: "https://github.com/chlwhdtn03/LMS-API/releases/download/<release-tag>/LmsApi.xcframework.zip",
checksum: "<checksum calculated for the ZIP file>"
)
]
)체크리스트:
Package.swift URL의 <release-tag>가 일치해야 합니다.checksum은 빌드 완료된 LmsApi.xcframework.zip 파일 기준으로 계산된 체크섬이어야 합니다.swift package compute-checksum LmsApi.xcframework.zipassembleLmsApiXCFramework는 Debug와 Release를 모두 빌드합니다. 배포용으로는 아래 Release 전용 태스크를 사용합니다. Kotlin/Native 링크 과정의 메모리 부족으로 Gradle 데몬이 종료되는 것을 방지하기 위해 gradle.properties에서 Gradle 힙을 4GB, worker 수를 2로 설정합니다. JVM garbage collector is thrashing 메시지가 나오면 org.gradle.jvmargs와 org.gradle.workers.max의 적용 값을 확인합니다.
./gradlew :library:assembleLmsApiReleaseXCFramework 실행LmsApi.xcframework를 LmsApi.xcframework.zip으로 압축swift package compute-checksum LmsApi.xcframework.zip 실행하여 체크섬 확보Package.swift의 checksum 값 및 URL 경로 업데이트 후 커밋LmsApi.xcframework.zip을 릴리스 에셋으로 업로드바이너리 프레임워크를 수동으로 내려받거나 직접 로컬 빌드하여 추가할 수도 있습니다.
./gradlew :library:assembleLmsApiReleaseXCFrameworklibrary/build/XCFrameworks/release/LmsApi.xcframework 폴더를 Xcode의 Project Navigator로 드래그하여 드롭합니다.General > Frameworks, Libraries, and Embedded Content 항목에서 LmsApi.xcframework를 등록합니다.Do Not Embed를 선택합니다.현재 소스 설정으로 직접 빌드한 XCFramework에는 실제 기기용 iosArm64 slice와 시뮬레이터용 iosX64 / iosSimulatorArm64 slice가 포함됩니다.
getTerms, getTodoList, getSubjects 및 모든 유세인트(U-Saint) API는 반드시 loginLMS 인증이 완료된 후에 정상 호출 가능합니다.object LmsApi가 싱글톤 객체로 변환되어 LmsApi.shared 형태로 접근합니다.LmsApi는 단일 사용자 세션을 공유하므로 같은 프로세스에서 여러 계정의 요청을 동시에 처리하는 용도로 사용할 수 없습니다.CyberApi는 숭실사이버대학교(KCU) 전용 객체로, 숭실대학교 LmsApi와 세션·토큰·쿠키를 전혀 공유하지 않습니다. 사이버대학교 기능 사용 전에는 반드시 CyberApi.login을 호출해야 합니다.object CyberApi 또한 싱글톤 객체로 변환되어 CyberApi.shared 형태로 접근합니다.loadingState 콜백 및 비동기 결과 수신 스레드는 메인(UI) 스레드를 보장하지 않습니다. SwiftUI/UIKit/Compose 등 화면 렌더링에 반영할 경우 메인 디스패처/스레드로의 컨텍스트 스위칭이 필요합니다.숭실대학교 LMS(Canvas, LearningX), 유세인트(U-Saint) 및 숭실사이버대학교(KCU)의 학기, 강의, 할 일, 출석, 공지, 수강편람, 시간표, 성적, 채플, 등록금, 장학금, 졸업사정표 등의 정보를 조회하기 위한 Kotlin Multiplatform 라이브러리입니다.
이 README는 **Swift Package Manager(SPM)**를 이용한 iOS 연동과 Gradle을 이용한 Android/Kotlin 연동을 기준으로 작성되었습니다.
iosArm64
macosArm64
현재 소스 빌드에는 JavaScript target이 포함되어 있지 않습니다. iOS는 실제 기기(iosArm64)와 시뮬레이터(iosX64, iosSimulatorArm64)를 지원합니다. iOS 앱 연동은 GitHub Release에 배포된 XCFramework를 사용하는 SPM 방식을 권장합니다.
LmsApi 싱글톤과 로그인·조회 콜백 API는 그대로 유지됩니다.parse*, merge*, find*, fetchWebDynproHtml 같은 원본 응답 처리 함수는 내부 구현으로 변경되었습니다. 앱에서는 이 함수들을 직접 호출하지 말고 get* 조회 API를 사용해야 합니다.LmsApi는 프로세스 내에서 로그인 세션, 토큰, 쿠키와 일부 최신 조회 조건을 공유합니다. 새 로그인을 시작하면 이전 사용자의 세션과 캐시를 먼저 비우며, logout도 로그인 상태와 사용자별 캐시를 제거합니다.getTerms, getTodoList, getSubjects 등)는 외부에 콜백 API로 제공됩니다. U-Saint 조회 API는 Kotlin의 suspend 함수와 결과 콜백을 모두 제공합니다.internal 서비스로 분리되었지만 외부 사용법은 변경되지 않았습니다. 내부 구조와 기능 추가 규칙은 내부 구현 가이드를 참고하세요.iOS 앱 프로젝트에서는 Xcode의 Swift Package Manager를 통해 의존성을 추가할 수 있습니다.
File > Add Package Dependencies...를 선택합니다.https://github.com/chlwhdtn03/LMS-API
LmsApi를 선택하고 앱 target에 추가합니다.Swift 파일에서는 다음과 같이 import 합니다.
import LmsApi[!NOTE] SPM은 내부적으로 GitHub Release에 업로드된
LmsApi.xcframework.zip을 내려받아 사용하도록 구성되어 있습니다. SPM에서 설치되는 바이너리 버전은 루트Package.swift의 Release URL을 따르며, Maven 배포 버전과 별도로 갱신될 수 있습니다.
Android 또는 Kotlin Multiplatform(KMP) 프로젝트에서는 Gradle 의존성으로 추가하여 사용할 수 있습니다.
Android 단일 프로젝트 (build.gradle.kts):
dependencies {
implementation("io.github.chlwhdtn03:lms:1.6.13")
}Kotlin Multiplatform 프로젝트 (commonMain 의존성):
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.chlwhdtn03:lms:1.6.13")
}
}
}LMS 조회 기능과 대부분의 유세인트(U-Saint) 조회 기능은 LMS 로그인 완료 후 생성된 세션을 공유하여 호출할 수 있습니다. 로그인에 성공하면 학번 정보와 토큰 정보가 내부적으로 캐싱되어 이후 호출되는 API에 자동으로 적용됩니다. 공개 수강편람 조회는 예외로, 로그인 없이 사용할 수 있습니다.
[!NOTE] 숭실사이버대학교(KCU)는 별도 도메인(
portal.kcu.ac,lms.kcu.ac)과 세션을 사용하므로,LmsApi와 무관하게 CyberApi를 통해 독립적으로 로그인하고 조회합니다.
import LmsApi
func performLogin() {
LmsApi.shared.loginLMS(id: "학번", password: "비밀번호") { result in
if result.success {
print("로그인 성공")
} else {
print("로그인 실패: \(result.errorMessage ?? "알 수 없는 오류")")
}
}
}import io.github.chlwhdtn03.LmsApi
fun performLogin() {
LmsApi.loginLMS(id = "학번", password = "비밀번호") { result ->
if (result.success) {
println("로그인 성공")
} else {
println("로그인 실패: ${result.errorMessage}")
}
}
}유세인트 조회 기능은 시간표, 성적, 채플, 등록금, 장학금, 졸업사정표 데이터를 비동기식으로 파싱하여 반환합니다. Kotlin에서는 suspend 함수 또는 결과 콜백을 사용할 수 있으며, iOS에서는 Kotlin/Native가 변환한 비동기 API를 Swift async/await 또는 결과 콜백 형태로 사용할 수 있습니다.
import LmsApi
func loadUSaintInformation() {
Task {
do {
// 1. 시간표 조회
let timetable = try await LmsApi.shared.getTimetable()
print("시간표 학기: \(timetable.year) \(timetable.semester)")
for item in timetable.items {
print("- [\(item.subject)] \(item.classroom) / \(item.professor)")
}
// 2. 성적 상세 조회 (year, semester에 nil을 전달하면 캐싱된 최근 학기 성적을 조회합니다.)
let gradeTable = try await LmsApi.shared.getGradeTable(year: nil, semester: nil)
for grade in gradeTable.items {
print("- \(grade.subjectName): \(grade.grade) (\(grade.credits)학점)")
}
// 3. 성적 요약 조회 (학기별 신청학점, 평점평균, 석차 등)
let gradeSummary = try await LmsApi.shared.getSemesterGradeSummaryTable()
for summary in gradeSummary.items {
print("- \(summary.year)년 \(summary.semester?.name ?? "")학기 평점: \(summary.gpa)")
}
// 4. 채플 출결 및 좌석 조회 (year, semester에 nil을 전달하면 캐싱된 최근 채플 내역을 조회합니다.)
let chapel = try await LmsApi.shared.getChapelTable(year: nil, semester: nil)
print("배정 좌석 번호: \(chapel.seatStatusTable.items.first?.seatNo ?? "없음")")
// 5. 등록금 납부 내역 조회
let tuition = try await LmsApi.shared.getTuitionTable()
for record in tuition.items {
print("- \(record.year) \(record.semester) 납부 금액: \(record.paymentAmount)")
}
// 6. 장학 수혜 내역 조회
let scholarship = try await LmsApi.shared.getScholarshipHistoryTable()
for item in scholarship.items {
print("- \(item.year)학년도 \(item.semester)학기 [\(item.scholarshipName)] 수혜 금액: \(item.actualAmount)")
}
// 7. 졸업사정표 조회
let graduate = try await LmsApi.shared.getGraduateTable()
for row in graduate.items {
print("- \(row.classification) (졸업요건: \(row.standardValue)학점 / 취득: \(row.calculatedValue)학점)")
}
} catch {
print("유세인트 정보 조회 실패: \(error.localizedDescription)")
}
}
}import io.github.chlwhdtn03.LmsApi
import io.github.chlwhdtn03.data.Lms.Semester
fun loadUSaintForAndroid() {
// 1. 시간표 조회
LmsApi.getTimetable { result ->
if (result.success && result.timetable != null) {
println("시간표 학기: ${result.timetable.year} ${result.timetable.semester}")
}
}
// 2. 성적 상세 조회
LmsApi.getGradeTable(year = "2026", semester = Semester.FIRST) { result ->
if (result.success && result.gradeTable != null) {
result.gradeTable.items.forEach { cell ->
println("${cell.subjectName}: ${cell.grade}")
}
}
}
// 3. 성적 요약 조회
LmsApi.getSemesterGradeSummaryTable { result ->
if (result.success && result.summaryTable != null) {
result.summaryTable.items.forEach { summary ->
println("${summary.year}학년도 평점평균: ${summary.gpa}")
}
}
}
// 4. 채플 조회
LmsApi.getChapelTable(year = null, semester = null) { result ->
if (result.success && result.chapelInformation != null) {
println("결석 횟수: ${result.chapelInformation.seatStatusTable.items.firstOrNull()?.absenceCount}")
}
}
// 5. 등록금 납부 내역 조회
LmsApi.getTuitionTable { result ->
if (result.success && result.tuitionTable != null) {
println("마지막 납부액: ${result.tuitionTable.items.firstOrNull()?.paymentAmount}")
}
}
// 6. 장학 수혜 내역 조회
LmsApi.getScholarshipHistoryTable { result ->
if (result.success && result.scholarshipHistoryTable != null) {
println("수혜 장학금명: ${result.scholarshipHistoryTable.items.firstOrNull()?.scholarshipName}")
}
}
// 7. 졸업사정표 조회
LmsApi.getGraduateTable { result ->
if (result.success && result.graduateTable != null) {
println("이수결과: ${result.graduateTable.items.firstOrNull()?.result}")
}
}
}수강편람은 조회 유형별 콤보박스가 서로 연동되므로 먼저 옵션을 받고, 선택한 key를
filterKeys에 같은 순서로 추가합니다. 검색 결과는 서버의 500줄 단위 페이지를 모두 합쳐
반환합니다.
val baseQuery = CourseCatalogQuery(
year = "2026",
semester = Semester.FIRST,
category = CourseCatalogCategory.DEPARTMENT,
)
val collegeOptions = LmsApi.getCourseCatalogSearchOptions(baseQuery)
val collegeKey = collegeOptions.filters[0].options.first().key
val departmentOptions = LmsApi.getCourseCatalogSearchOptions(
baseQuery.copy(filterKeys = listOf(collegeKey)),
)
val departmentKey = departmentOptions.filters[1].options.first().key
val courses = LmsApi.getCourseCatalogTable(
baseQuery.copy(filterKeys = listOf(collegeKey, departmentKey)),
)
// 목록 조회 단계에서는 OZ Viewer를 실행하지 않습니다.
// 사용자가 특정 강좌의 계획 버튼을 누른 시점에만 PDF를 만듭니다.
val selectedCourse = courses.items.first()
val pdfBytes: ByteArray = selectedCourse.loadPlan()
// 예비수강신청도 로그인 후 조회한 과목에서 동일하게 사용합니다.
val preRegistration = LmsApi.getPreRegistrationTable()
val preRegistrationPdf: ByteArray = preRegistration.items.first().loadPlan()CourseCatalogCategory.SUBJECT와 PROFESSOR는 keyword를 사용합니다. 버튼형 계획서는
재사용할 수 있는 링크가 아니므로 목록의 plan은 빈 문자열일 수 있습니다. Android와
iOS에서는 과목의 loadPlan()을 호출하면 내부에서 일회성 OZ Viewer를 새로 열고 PDF가
완성된 뒤 바이트를 반환합니다. 이 작업은 네트워크 요청과 보고서 렌더링을 포함하므로 UI
로딩 상태를 표시하는 것이 좋습니다. PDF 크기와 생성 메모리를 줄이기 위해 폰트 임베딩은
비활성화되어 있으므로, 특수 폰트가 사용된 계획서는 플랫폼의 대체 폰트로 표시될 수 있습니다.
Android에서는 OZ 콜백의 전체 문자열을 Kotlin으로 넘기지 않고 WebView의 ArrayBuffer를
ByteArray로 직접 수신합니다. JVM과 macOS에서는 PDF 로드를 지원하지 않습니다.
iOS XCFramework에서는 loadPlan(completionHandler:)로 노출되며 결과 형식은
KotlinByteArray입니다. Swift의 Data로 변환하면 Apple 기본 프레임워크인
PDFKit의 PDFDocument(data:)와
PDFView로 앱 내부 네이티브 PDF 뷰어를 구성할 수 있습니다. 별도의 외부 PDF 뷰어
라이브러리는 필요하지 않습니다.
import LmsApi
import PDFKit
extension KotlinByteArray {
func toData() -> Data {
Data((0..<Int(size)).map { index in
UInt8(bitPattern: get(index: Int32(index)))
})
}
}
selectedCourse.loadPlan { bytes, error in
guard let bytes else {
print(error?.localizedDescription ?? "강의계획서 로드 실패")
return
}
let data = bytes.toData()
guard let document = PDFDocument(data: data) else {
print("올바른 PDF 데이터가 아닙니다.")
return
}
DispatchQueue.main.async {
pdfView.document = document
pdfView.autoScales = true
pdfView.displayMode = .singlePageContinuous
}
}SwiftUI에서는 PDFView를 UIViewRepresentable로 감싸서 사용할 수 있습니다.
import SwiftUI
import PDFKit
struct NativePDFView: UIViewRepresentable {
let data: Data
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.autoScales = true
view.displayMode = .singlePageContinuous
view.displayDirection = .vertical
view.document = PDFDocument(data: data)
return view
}
func updateUIView(_ view: PDFView, context: Context) {
view.document = PDFDocument(data: data)
}
}KotlinByteArray.toData()는 Swift에서 각 바이트를 읽어 새 Data를 만드는 과정이므로 PDF가
큰 경우 추가 복사 비용이 발생합니다. 변환 중에도 로딩 상태를 유지하고, 같은 PDF는 변환한
Data 또는 임시 파일을 재사용하는 것이 좋습니다. 추후 iOS 전용 API가 NSData를 직접
반환하도록 추가된다면 이 변환 단계는 생략할 수 있지만, 현재 공개 API의 반환 타입은
KotlinByteArray입니다.
LMS 조회 기능은 학기 목록, 강의 목록, 할 일(과제 및 동영상 시청 기한), 출석, 공지, 제출 및 점수 정보를 조회할 수 있는 기능을 제공합니다.
import LmsApi
func loadLMSTodos() {
LmsApi.shared.getTerms { termsResult in
guard termsResult.success else {
print("학기 조회 실패: \(termsResult.errorMessage ?? "알 수 없는 오류")")
return
}
guard let latestTerm = termsResult.terms.last else {
print("조회 가능한 학기가 없습니다.")
return
}
LmsApi.shared.getTodoList(
term: latestTerm,
loadingState: { progress in
print("진행률: \(Int(progress.floatValue * 100))%")
},
postHogDistinctId: nil
) { subjectsResult in
guard subjectsResult.success else {
print("할 일 조회 실패: \(subjectsResult.errorMessage ?? "알 수 없는 오류")")
return
}
for subject in subjectsResult.subjects {
print("과목: \(subject.name)")
for todo in subject.todoList {
print("- \(todo.title) (마감일: \(todo.due_date))")
}
}
}
}
}import io.github.chlwhdtn03.LmsApi
fun loadLMSForAndroid() {
LmsApi.getTerms { termsResult ->
if (termsResult.success) {
val latestTerm = termsResult.terms.lastOrNull() ?: return@getTerms
LmsApi.getTodoList(
term = latestTerm,
loadingState = { progress ->
println("loading: ${(progress * 100).toInt()}%")
},
completion = { subjectsResult ->
if (subjectsResult.success) {
subjectsResult.subjects.forEach { subject ->
subject.todoList.forEach { todo ->
println("[${subject.name}] ${todo.title} / ${todo.due_date}")
}
}
}
}
)
}
}
}숭실사이버대학교 포털(portal.kcu.ac) 및 LMS(lms.kcu.ac)에 로그인하여 수강 과목 목록과 주차별 강의 수강일람(출석 현황, 진도율, 학습 시간, 완료 여부 등)을 조회할 수 있습니다.
CyberApi는 숭실대학교 LMS 및 유세인트(LmsApi)와 별개의 도메인과 시스템을 사용하므로, 세션 및 쿠키를 공유하지 않는 독립적인 싱글톤 객체입니다.
iOS(Swift)에서는 Kotlin object CyberApi가 CyberApi.shared로 접근되며, async/await 또는 결과 콜백 방식을 모두 지원합니다.
import LmsApi
func loadCyberUniversityLectures() {
Task {
do {
// 1. 사이버대학교 포털 로그인
let loginSuccess = try await CyberApi.shared.login(id: "아이디", password: "비밀번호")
guard loginSuccess.boolValue else {
print("로그인 실패")
return
}
// 2. 이번 학기 수강과목 목록 조회
let subjects = try await CyberApi.shared.getSubjects()
for subject in subjects {
print("과목: \(subject.name) (\(subject.credit)학점) - 진도율: \(subject.progressPercent)%")
}
// 3. 첫 번째 과목의 주차별 수강일람 조회
if let firstSubject = subjects.first {
let weeks = try await CyberApi.shared.getWeeklyLectures(subject: firstSubject)
for week in weeks {
print("[\(week.weekNo)주차] \(week.topic) - 출석상태: \(week.attendanceStatus) (기간: \(week.attendancePeriod))")
for lecture in week.lectures {
print(" - \(lecture.lectureNo)강: \(lecture.statusText), 진도율: \(lecture.progressPercent)%, 학습시간: \(lecture.studyTime)/\(lecture.baseTime), 완료여부: \(lecture.isCompleted)")
}
}
}
// 4. 로그아웃 (세션 정리)
try await CyberApi.shared.logout()
} catch {
print("사이버대학교 정보 조회 실패: \(error.localizedDescription)")
}
}
}import LmsApi
func loadCyberWithCallback() {
CyberApi.shared.login(id: "아이디", password: "비밀번호") { loginResult in
guard loginResult.success else {
print("로그인 실패: \(loginResult.errorMessage ?? "알 수 없는 오류")")
return
}
CyberApi.shared.getSubjects { subjectsResult in
guard subjectsResult.success else {
print("수강과목 조회 실패: \(subjectsResult.errorMessage ?? "알 수 없는 오류")")
return
}
if let firstSubject = subjectsResult.subjects.first {
CyberApi.shared.getWeeklyLectures(subject: firstSubject) { lecturesResult in
if lecturesResult.success {
print("주차 수: \(lecturesResult.weeks.count)")
}
}
}
}
}
}Android/Kotlin에서는 코루틴(suspend) 또는 결과 콜백 오버로드를 모두 지원합니다.
import io.github.chlwhdtn03.CyberApi
suspend fun loadCyberUniversityForAndroid() {
try {
// 1. 사이버대학교 포털 로그인
val loginSuccess = CyberApi.login(id = "아이디", password = "비밀번호")
if (!loginSuccess) {
println("로그인 실패")
return
}
// 2. 이번 학기 수강과목 목록 조회
val subjects = CyberApi.getSubjects()
subjects.forEach { subject ->
println("[${subject.name}] 교수: ${subject.professor}, 학점: ${subject.credit}, 진도율: ${subject.progressPercent}%")
}
// 3. 특정 과목의 주차별 수강일람 조회
val firstSubject = subjects.firstOrNull() ?: return
val weeks = CyberApi.getWeeklyLectures(firstSubject)
weeks.forEach { week ->
println("${week.weekNo}주차: ${week.topic} (${week.attendancePeriod}) [${week.attendanceStatus}]")
week.lectures.forEach { lecture ->
println(" - ${lecture.lectureNo}강: ${lecture.statusText}, 진도율: ${lecture.progressPercent}%, 완료여부: ${lecture.isCompleted}")
}
}
// 4. 로그아웃
CyberApi.logout()
} catch (e: Exception) {
println("사이버대학교 조회 중 오류 발생: ${e.message}")
}
}import io.github.chlwhdtn03.CyberApi
fun loadCyberWithCallback() {
CyberApi.login(id = "아이디", password = "비밀번호") { loginResult ->
if (loginResult.success) {
CyberApi.getSubjects { subjectsResult ->
if (subjectsResult.success) {
val firstSubject = subjectsResult.subjects.firstOrNull() ?: return@getSubjects
CyberApi.getWeeklyLectures(firstSubject) { lecturesResult ->
if (lecturesResult.success) {
println("주차 수: ${lecturesResult.weeks.size}")
}
}
}
}
} else {
println("로그인 실패: ${loginResult.errorMessage}")
}
}
}fun loginLMS(id: String, password: String, completion: (LmsLoginResult) -> Unit)LMS 아이디와 비밀번호로 로그인합니다. 새 로그인 시도 전에 기존 세션을 제거하며, 로그인 후 사용자 정보가 실제로 조회된 경우에만 성공 처리합니다. 성공 시 유세인트 세션도 자동으로 공유됩니다.
로그인 여부는 외부에서 읽기만 가능한 LmsApi.isLoggined로 확인할 수 있습니다. 기존 호환성을 위해 현재 철자를 유지합니다.
fun logout(completion: () -> Unit)현재 로그인 상태와 쿠키, 사용자별 성적·채플 최신 조회 조건 캐시를 제거합니다.
fun getTerms(completion: (LmsTermsResult) -> Unit)로그인된 사용자의 학기 목록을 가져옵니다.
fun getTodoList(term: Term, loadingState: (Float) -> Unit = {}, completion: (LmsSubjectsResult) -> Unit)
fun getTodoList(
term: Term,
loadingState: (Float) -> Unit = {},
postHogDistinctId: String?,
completion: (LmsSubjectsResult) -> Unit,
)과목 기본 정보와 할 일 목록(과제, 동영상 등), 제출 정보를 빠르게 파싱하여 가져옵니다.
postHogDistinctId가 없는 오버로드를 사용하거나 null을 전달하면 분석 데이터를 전송하지 않습니다. 식별자를 전달하면 식별자별 하루 한 번 전송 여부를 20% 비율로 샘플링하며, 선택된 경우 Todo 동기화 통계와 항목 상태 스냅샷을 PostHog로 전송합니다. 이를 의도한 앱에서만 사용하세요.
fun getSubjects(term: Term, loadingState: (Float) -> Unit = {}, completion: (LmsSubjectsResult) -> Unit)과목 기본 정보, 할 일, 출석, 공지, 제출 및 점수 데이터를 모두 가져옵니다. 여러 API를 내부적으로 호출하므로 getTodoList에 비해 무겁습니다.
fun getLoginInfo(completion: (LmsLoginInfoResult) -> Unit)로그인한 학생의 이름, 학과, 로그인 ID, 이메일 등의 기본 신원 정보를 가져옵니다.
fun getCookies(completion: (LmsCookiesResult) -> Unit)현재 로그인된 세션의 쿠키 목록을 반환합니다. 외부 서비스와의 연동 시 사용합니다.
// 창업지원단 공지사항 조회
fun loadStartUpNotices(pageNum: Int = 1, completion: (StartUpNoticesResult) -> Unit)
// 장학 공지사항 조회
fun loadScholarships(pageNum: Int = 1, completion: (ScholarshipNoticesResult) -> Unit)suspend fun getTimetable(): Timetable
suspend fun getTimetable(year: String?, semester: Semester?): Timetable
fun getTimetable(completion: (LmsTimetableResult) -> Unit)
fun getTimetable(year: String?, semester: Semester?, completion: (LmsTimetableResult) -> Unit)개인의 유세인트 시간표를 조회합니다. 학년도와 학기를 생략하면 유세인트가 제공하는 기본 조회 기간을 사용합니다.
suspend fun getGradeTable(year: String? = null, semester: Semester? = null): GradeTable
fun getGradeTable(completion: (LmsGradeResult) -> Unit)
fun getGradeTable(year: String?, semester: Semester?, completion: (LmsGradeResult) -> Unit)지정된 학년도 및 학기의 성적 상세 내역을 조회합니다. 파라미터가 모두 null일 경우 캐싱된 최신 학기 성적을 반환합니다.
suspend fun getSemesterGradeSummaryTable(): SemesterGradeSummaryTable
fun getSemesterGradeSummaryTable(completion: (LmsSemesterGradeSummaryResult) -> Unit)전체 학기별 신청학점, 취득학점, 평점평균 및 석차 정보가 담긴 성적 요약 데이터를 조회합니다.
suspend fun getChapelTable(year: String? = null, semester: Semester? = null): ChapelInformation
fun getChapelTable(completion: (LmsChapelResult) -> Unit)
fun getChapelTable(year: String?, semester: Semester?, completion: (LmsChapelResult) -> Unit)지정된 학년도 및 학기의 채플 정보(좌석 번호, 주차별 출결, 결석계 신청 현황)를 조회합니다. (계절학기 조회 불가)
suspend fun getTuitionTable(): TuitionTable
fun getTuitionTable(completion: (LmsTuitionResult) -> Unit)학년도/학기별 등록금 고지액, 장학 감면액, 실납부일자 및 납부 금액 등 등록금 납부 이력을 조회합니다.
suspend fun getScholarshipHistoryTable(): ScholarshipHistoryTable
fun getScholarshipHistoryTable(completion: (LmsScholarshipHistoryResult) -> Unit)학기별 수혜한 장학금 명칭, 지급 방법, 선발 금액 및 실수혜금액 등의 내역을 조회합니다.
suspend fun getGraduateTable(): GraduateTable
fun getGraduateTable(completion: (LmsGraduateTableResult) -> Unit)졸업사정표 상의 이수구분별 졸업 기준 요건 학점, 본인 취득학점, 차이값 및 판정 결과를 조회합니다.
숭실사이버대학교 전용 API로, LmsApi와는 별도의 독립적인 싱글톤 객체입니다.
suspend fun login(id: String, password: String): Boolean
fun login(id: String, password: String, completion: (CyberLoginResult) -> Unit)사이버대학교 포털(portal.kcu.ac)에 로그인합니다. 내부적으로 RSA 공개키를 조회하여 비밀번호를 암호화한 뒤 로그인을 수행하며, 성공 시 세션 쿠키를 보관합니다.
로그인 여부는 외부에서 읽기 가능한 CyberApi.isLoggined 프로퍼티로 확인할 수 있습니다.
suspend fun logout()
fun logout(completion: () -> Unit)현재 사이버대학교 로그인 세션을 종료하고 로그인 상태를 해제합니다.
suspend fun getSubjects(): List<CyberSubject>
suspend fun getSubjects(postHogDistinctId: String?): List<CyberSubject>
fun getSubjects(completion: (CyberSubjectsResult) -> Unit)
fun getSubjects(postHogDistinctId: String?, completion: (CyberSubjectsResult) -> Unit)로그인된 사용자의 이번 학기 수강과목 목록(CyberSubject)을 가져옵니다. 과목명, 이수구분, 담당 교수, 학점, 전체 진도율 등이 포함되어 있습니다. (로그인 필수)
postHogDistinctId를 전달하면 전체 과목의 주차별 강의와 학습평가를 추가 조회한 뒤 todo_snapshot_cyber와 $identify를 함께 전송합니다. LMS와 동일하게 식별자별 UTC 날짜 기준 하루 한 번 전송 여부를 20% 확률로 샘플링하며, 전송 이력은 LMS와 별개인 메모리 캐시입니다. 식별자를 생략하거나 null/빈 문자열을 전달하면 추가 조회와 전송이 없습니다. 기존 앱에서 분석을 활성화하려면 CyberApi.getSubjects(postHogDistinctId = distinctId)를 호출하세요.
스냅샷 속성은 LMS와 동일하며, 동영상은 진도율 100% 이상이면 완료, 퀴즈/과제는 제출·응시 완료 상태이면 완료로 계산합니다. 미제출은 미완료이면서 마감이 지난 항목이고, 전체 개수에는 기한이 남은 항목도 포함됩니다. 시험/토론은 제외하며 본제출/재제출 행은 중복 집계하지 않습니다. 사이버 날짜는 한국 시간으로 해석하고 출석인정기간은 종료일 끝까지 인정합니다. 과목 코드는 문자열이므로 items.course_id도 문자열입니다.
$identify는 LMS와 사이버가 공유하는 PostHog 기본 식별 이벤트이며 $set/$set_once로 사용자 속성을 갱신합니다. 속성명은 LMS와 동일하게 유지하며 _cyber는 스냅샷 이벤트명에만 붙입니다. 동일한 식별자를 전달하면 최초/마지막 동기화 사용자 속성은 LMS와 사이버가 공유하므로, 서비스별 미제출률은 각 스냅샷 이벤트의 snapshot_unsubmitted_ratio로 분석하세요. 전체 과목 조회가 실패하면 스냅샷을 전송하지 않으며 조회 오류를 반환합니다. PostHog 전송 실패는 LMS와 동일하게 조회 결과에 영향을 주지 않습니다.
suspend fun getWeeklyLectures(subject: CyberSubject): List<CyberWeek>
fun getWeeklyLectures(subject: CyberSubject, completion: (CyberWeeklyLecturesResult) -> Unit)특정 수강과목의 주차별 수강일람(CyberWeek) 및 하위 강의 목록(CyberLecture)을 조회합니다. 각 주차별 출석인정기간, 출석 상태, 강의별 학습 시간, 진도율, 완료 여부(isCompleted)를 확인할 수 있습니다. (로그인 필수)
FIRST: 1학기 (코드: "090")SUMMER: 여름학기 (코드: "091")SECOND: 2학기 (코드: "092")WINTER: 겨울학기 (코드: "093")year: 학년도 (예: "2026학년도")semester: 학기 (예: "1학기")items: TimetableCell 리스트
dayOfWeek: 요일 (DayOfWeek Enum)period: 교시 (예: "1 교시")periodTime: 교시 시간 범위 (예: "(08:00-08:50)")subject: 과목명professor: 교수명time: 강의 시간 문자열classroom: 강의실year: 학년도semester: 학기items: GradeCell 리스트
subjectCode: 과목코드subjectName: 과목명classification: 이수구분 (예: "전공기초")credits: 학점 (예: "3.0")grade: 등급 (예: "A+")gradePoint: 평점 (예: "4.5")professor: 교수명items: SemesterGradeSummaryCell 리스트
year: 학년도semester: 학기attemptedCredits: 신청학점earnedCredits: 취득학점pfCredits: P/F학점gpa: 평점평균gpaSum: 평점계arithmeticMean: 산술평균semesterRank: 학기석차totalRank: 전체석차academicWarning: 학사경고여부consultationStatus: 상담여부failedYearStatus: 유급여부year: 학년도semester: 학기seatStatusTable: 좌석 정보 테이블 (classGroup, timetable, classroom, seatNo, absenceCount, gradeResult)attendanceTable: 출결 현황 테이블 (classGroup, date, lectureType, status)absenceTable: 결석계 신청 이력 테이블 (year, semester, detail)items: TuitionCell 리스트
year, semester: 학년도 및 학기grade: 학년(기)registrationType: 등록구분 (예: "정규등록")registrationDate: 납부일자amount: 고지금액reduction: 장학 감면액paymentAmount: 최종 실납부금액items: ScholarshipHistoryCell 리스트
year, semester: 장학금 지급 학년도 및 학기scholarshipName: 장학금명paymentMethod: 지급방법 (예: "고지서 감면")processStatus: 처리 상태selectedAmount: 선발금액actualAmount: 실수혜금액redeemedAmount: 환수금액replacedAmount: 교체금액replacedScholarshipName: 교체장학금명workDepartment: 근로부서items: GraduateTableCell 리스트
classification: 이수구분 (예: "전공선택")requirement: 졸업요건standardValue: 기준학점calculatedValue: 취득학점difference: 차이값result: 이수 여부 판정 (예: "합격", "미필")usedSubjects: 해당 요건에 사용된 과목명 리스트 (과목사용 열을 쉼표 단위로 분리)id: 학기 고유 IDname: 학기명start_at: 시작 시각end_at: 종료 시각id: 과목 고유 IDtermId: 학기 IDtermName: 학기명name: 과목명professor: 담당 교수totalStudents: 수강 인원todoList: 과제 및 할 일 목록attendances: 출석 기록 리스트discussions: 공지사항 목록submissions: 과제 제출 정보scoredAssignments: 평가 및 획득한 점수 리스트component_type: 항목 타입 (예: assignment, commons)assignment_id: 과제 고유 IDtitle: 제목due_date: 마감 기한assignment_id: 과제 고유 IDattachments: 첨부파일 목록attempt: 제출 시도 횟수cached_due_date: 마감 시각late: 지각 여부preview_url: 제출 파일 미리보기 URLsubmitted_at: 제출 시각submission_type: 제출 유형workflow_state: 제출 상태 (예: submitted, graded, unsubmitted)score: 획득 점수name: 과목명category: 이수구분 (예: "전공기초")professor: 담당 교수credit: 학점 (예: "3.0")progressPercent: 전체 진도율 (%)year: 학년도semesterCode: 학기 코드courseCode: 과목 코드deptCode: 학과 코드userNo: 사용자 번호weekNo: 주차 번호 (예: 1)attendancePeriod: 출석인정기간 (예: "2026.09.01 ~ 2026.09.14")topic: 주차 주제 / 강의명attendanceStatus: 출석 인정 상태 (예: "출석", "결석")lectures: 주차 내 개별 강의 리스트 (List<CyberLecture>)lectureNo: 강 번호 (예: 1)statusText: 학습 상태 텍스트 (예: "학습완료", "미학습")progressPercent: 진도율 (%)studyTime: 학습 시간 (예: "35분 20초")baseTime: 인정 기준 시간 (예: "30분 00초")videoFilePath: 동영상 파일 경로 (nullable)audioFilePath: 오디오 파일 경로 (nullable)isCompleted: 학습 완료 여부 계산 프로퍼티 (progressPercent >= 100)파서와 공개 범위 등 외부 서버가 필요 없는 테스트는 일반 JVM 테스트로 실행합니다.
./gradlew :library:jvmTest실제 LMS/U-Saint 네트워크 API와 각 콜백 오버로드를 함께 검증하려면 테스트 계정을 환경 변수로 전달합니다. 계정 정보가 없으면 통합 테스트는 외부 서버를 호출하지 않고 종료됩니다.
LMS_TEST_ID="학번" \
LMS_TEST_PASSWORD="비밀번호" \
./gradlew :library:jvmTest \
--tests "io.github.chlwhdtn03.LmsApiFullIntegrationTest"필요하면 LMS_TEST_TERM_ID, LMS_TEST_YEAR, LMS_TEST_SEMESTER도 지정할 수 있습니다. LMS_TEST_SEMESTER는 FIRST, SECOND, 090, 092, 1학기, 2학기 형식을 지원합니다. 실제 계정 정보는 소스 코드나 커밋에 저장하지 마세요.
사이버대학교(KCU) 계정으로 실제 서버와의 로그인 및 수강과목/수강일람 조회를 검증하려면 아래 환경 변수를 전달합니다:
CYBER_TEST_ID="사이버대학_아이디" \
CYBER_TEST_PASSWORD="사이버대학_비밀번호" \
./gradlew :library:jvmTest \
--tests "io.github.chlwhdtn03.CyberApiFullIntegrationTest" -i이 저장소의 루트에 있는 Package.swift는 Kotlin/Native 컴파일 결과물인 XCFramework를 바이너리 타겟으로 래핑하여 배포하도록 설정되어 있습니다.
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "LmsApi",
platforms: [
.iOS(.v14),
],
products: [
.library(name: "LmsApi", targets: ["LmsApi"])
],
targets: [
.binaryTarget(
name: "LmsApi",
url: "https://github.com/chlwhdtn03/LMS-API/releases/download/<release-tag>/LmsApi.xcframework.zip",
checksum: "<checksum calculated for the ZIP file>"
)
]
)체크리스트:
Package.swift URL의 <release-tag>가 일치해야 합니다.checksum은 빌드 완료된 LmsApi.xcframework.zip 파일 기준으로 계산된 체크섬이어야 합니다.swift package compute-checksum LmsApi.xcframework.zipassembleLmsApiXCFramework는 Debug와 Release를 모두 빌드합니다. 배포용으로는 아래 Release 전용 태스크를 사용합니다. Kotlin/Native 링크 과정의 메모리 부족으로 Gradle 데몬이 종료되는 것을 방지하기 위해 gradle.properties에서 Gradle 힙을 4GB, worker 수를 2로 설정합니다. JVM garbage collector is thrashing 메시지가 나오면 org.gradle.jvmargs와 org.gradle.workers.max의 적용 값을 확인합니다.
./gradlew :library:assembleLmsApiReleaseXCFramework 실행LmsApi.xcframework를 LmsApi.xcframework.zip으로 압축swift package compute-checksum LmsApi.xcframework.zip 실행하여 체크섬 확보Package.swift의 checksum 값 및 URL 경로 업데이트 후 커밋LmsApi.xcframework.zip을 릴리스 에셋으로 업로드바이너리 프레임워크를 수동으로 내려받거나 직접 로컬 빌드하여 추가할 수도 있습니다.
./gradlew :library:assembleLmsApiReleaseXCFrameworklibrary/build/XCFrameworks/release/LmsApi.xcframework 폴더를 Xcode의 Project Navigator로 드래그하여 드롭합니다.General > Frameworks, Libraries, and Embedded Content 항목에서 LmsApi.xcframework를 등록합니다.Do Not Embed를 선택합니다.현재 소스 설정으로 직접 빌드한 XCFramework에는 실제 기기용 iosArm64 slice와 시뮬레이터용 iosX64 / iosSimulatorArm64 slice가 포함됩니다.
getTerms, getTodoList, getSubjects 및 모든 유세인트(U-Saint) API는 반드시 loginLMS 인증이 완료된 후에 정상 호출 가능합니다.object LmsApi가 싱글톤 객체로 변환되어 LmsApi.shared 형태로 접근합니다.LmsApi는 단일 사용자 세션을 공유하므로 같은 프로세스에서 여러 계정의 요청을 동시에 처리하는 용도로 사용할 수 없습니다.CyberApi는 숭실사이버대학교(KCU) 전용 객체로, 숭실대학교 LmsApi와 세션·토큰·쿠키를 전혀 공유하지 않습니다. 사이버대학교 기능 사용 전에는 반드시 CyberApi.login을 호출해야 합니다.object CyberApi 또한 싱글톤 객체로 변환되어 CyberApi.shared 형태로 접근합니다.loadingState 콜백 및 비동기 결과 수신 스레드는 메인(UI) 스레드를 보장하지 않습니다. SwiftUI/UIKit/Compose 등 화면 렌더링에 반영할 경우 메인 디스패처/스레드로의 컨텍스트 스위칭이 필요합니다.