Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,6 @@ app/.idea/
/composeApp/google-services.json
/iosApp/iosApp/GoogleService-Info.plist
/iosApp/Configuration/Config.xcconfig

# Cloned reference repo
/chukchuk-haksa/
Comment on lines +50 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify if /chukchuk-haksa/ exists and is a Git repository

# Check if the directory exists
if [ -d "chukchuk-haksa" ]; then
  echo "Directory exists"
  # Check if it's a Git repository
  if [ -d "chukchuk-haksa/.git" ]; then
    echo "It's a Git repository"
    cd chukchuk-haksa
    git remote -v
  else
    echo "Not a Git repository"
  fi
else
  echo "Directory does not exist yet"
fi

Repository: cchaksa/cchaksa-app

Length of output: 1790


참조 저장소를 프로젝트 외부에 복제하는 것을 권장합니다.

.gitignore/chukchuk-haksa/ 디렉토리를 추가하는 것은 문법적으로 올바르지만, 참조용 저장소를 현재 프로젝트의 작업 디렉토리 내부에 복제하는 것은 권장되지 않습니다.

더 나은 접근 방법:

  • 참조 저장소를 프로젝트 디렉토리 외부(예: ../chukchuk-haksa/)에 복제하여 작업 공간을 깨끗하게 유지
  • 실제로 통합이 필요한 경우 Git 서브모듈 사용 검토
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 50 - 52, The .gitignore currently contains an entry
'/chukchuk-haksa/' which indicates a cloned reference repo inside the project;
remove that entry and do not keep a copy of the reference repository inside the
project tree — instead clone the reference repo outside the project (e.g.,
../chukchuk-haksa/) or, if you need it tracked/linked, add it as a Git
submodule; update repository docs/README to document how to obtain the reference
repo rather than ignoring '/chukchuk-haksa/' in .gitignore.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.chukchukhaksa.mobile.data.portal.datasource

import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult

interface PortalRemoteDataSource {
suspend fun login(username: String, password: String)
suspend fun startScraping(): ScrapingResult
suspend fun refreshScraping(): ScrapingResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.chukchukhaksa.mobile.data.portal.di

import com.chukchukhaksa.mobile.data.portal.datasource.PortalRemoteDataSource
import com.chukchukhaksa.mobile.data.portal.repository.PortalRepositoryImpl
import com.chukchukhaksa.mobile.domain.portal.repository.PortalRepository
import com.chukchukhaksa.mobile.remote.portal.PortalRemoteDataSourceImpl
import org.koin.dsl.module

val portalRepositoryModule = module {
single<PortalRemoteDataSource> { PortalRemoteDataSourceImpl(get()) }
single<PortalRepository> { PortalRepositoryImpl(get()) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.chukchukhaksa.mobile.data.portal.repository

import com.chukchukhaksa.mobile.data.portal.datasource.PortalRemoteDataSource
import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult
import com.chukchukhaksa.mobile.domain.portal.repository.PortalRepository

class PortalRepositoryImpl(
private val portalRemoteDataSource: PortalRemoteDataSource,
) : PortalRepository {

override suspend fun login(username: String, password: String) {
portalRemoteDataSource.login(username, password)
}

override suspend fun startScraping(): ScrapingResult {
return portalRemoteDataSource.startScraping()
}

override suspend fun refreshScraping(): ScrapingResult {
return portalRemoteDataSource.refreshScraping()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import com.chukchukhaksa.mobile.domain.auth.usecase.AppleLoginUseCase
import com.chukchukhaksa.mobile.domain.auth.usecase.CheckAuthStateUseCase
import com.chukchukhaksa.mobile.domain.auth.usecase.KakaoLoginUseCase
import com.chukchukhaksa.mobile.domain.config.usecase.CheckNeedForceUpdateUseCase
import com.chukchukhaksa.mobile.domain.portal.usecase.PortalLoginUseCase
import com.chukchukhaksa.mobile.domain.portal.usecase.RefreshPortalScrapingUseCase
import com.chukchukhaksa.mobile.domain.portal.usecase.StartPortalScrapingUseCase
import com.chukchukhaksa.mobile.domain.profile.usecase.GetProfileUseCase
import com.chukchukhaksa.mobile.domain.timetable.usecase.DeleteTimetableCellUseCase
import com.chukchukhaksa.mobile.domain.timetable.usecase.DeleteTimetableUseCase
Expand Down Expand Up @@ -58,4 +61,9 @@ val domainModule = module {
// Academic use cases
factory { GetAcademicSummaryUseCase(get()) }
factory { GetAcademicRecordUseCase(get()) }

// Portal use cases
factory { PortalLoginUseCase(get()) }
factory { StartPortalScrapingUseCase(get()) }
factory { RefreshPortalScrapingUseCase(get()) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: 포털 UseCase 등록이 별도 feature 모듈로 분리되어 있는지 확인
# 기대결과: Portal 관련 UseCase factory가 전용 Module.kt에 존재하고, 중앙 모듈은 조합만 수행

fd -i 'Module.kt' composeApp/src | sort
rg -n --iglob '*Module.kt' 'PortalLoginUseCase|StartPortalScrapingUseCase|RefreshPortalScrapingUseCase'

Repository: cchaksa/cchaksa-app

Length of output: 2193


🏁 Script executed:

cat -n composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/di/DomainModules.kt | head -80

Repository: cchaksa/cchaksa-app

Length of output: 3891


🏁 Script executed:

# Search for portal-related domain modules
fd -i 'portal' composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile | grep -i 'di\|module'

Repository: cchaksa/cchaksa-app

Length of output: 161


🏁 Script executed:

# Search for the UseCase files to understand their locations
fd -type f -i 'usecase.kt' composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile | head -20

Repository: cchaksa/cchaksa-app

Length of output: 233


🏁 Script executed:

# Check if separate domain modules exist for other features
fd -i 'domain.*module' composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile

Repository: cchaksa/cchaksa-app

Length of output: 140


🏁 Script executed:

# Check which modules are imported in the main module/app setup
fd -i 'app.*module\|main.*module' composeApp/src | head -10

Repository: cchaksa/cchaksa-app

Length of output: 45


🏁 Script executed:

# Look at the domain structure for portal vs academic to compare
tree -L 3 composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/portal/
tree -L 3 composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/academic/

Repository: cchaksa/cchaksa-app

Length of output: 653


포털 도메인 UseCase를 전용 모듈로 분리하세요.

현재 포털 UseCase 등록이 중앙 DomainModules.kt에 직접 포함되어 있어 feature-based 모듈 구성 원칙을 위반합니다. 포털 Repository는 이미 PortalRepositoryModule.kt로 분리되어 있으나, 도메인 계층 UseCase는 여전히 중앙화되어 있습니다. portalDomainModule을 생성하고 조합 모듈에서 통합하는 방식으로 개선해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/di/DomainModules.kt`
around lines 65 - 68, 포털 도메인 UseCase들이 중앙 DomainModules.kt에 직접 등록되어 있어
feature-based 모듈화 원칙을 위반하므로 PortalLoginUseCase, StartPortalScrapingUseCase,
RefreshPortalScrapingUseCase에 대한 팩토리 등록을 새로운 portalDomainModule로 분리하고 기존
DomainModules.kt(또는 조합 모듈)에서는 해당 portalDomainModule을 포함하도록 변경하세요; 구체적으로
PortalLoginUseCase/StartPortalScrapingUseCase/RefreshPortalScrapingUseCase 팩토리들을
새로운 파일에 정의한 Koin 모듈인 portalDomainModule로 옮기고 기존의 포털 리포지토리
모듈(PortalRepositoryModule.kt)과 함께 조합 모듈에서 불러오도록 통합하세요.

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.chukchukhaksa.mobile.data.auth.di.authRepositoryModule
import com.chukchukhaksa.mobile.data.config.di.appConfigRepositoryModule
import com.chukchukhaksa.mobile.data.openlecture.di.openLectureRepositoryModule
import com.chukchukhaksa.mobile.data.openmajor.di.openMajorRepositoryModule
import com.chukchukhaksa.mobile.data.portal.di.portalRepositoryModule
import com.chukchukhaksa.mobile.data.profile.di.ProfileRepositoryModule
import com.chukchukhaksa.mobile.data.timetable.di.timetableRepositoryModule
import com.chukchukhaksa.mobile.local.database.common.di.openLectureDatabaseModule
Expand Down Expand Up @@ -48,6 +49,7 @@ fun initKoin(config: KoinAppDeclaration? = null) {
authRepositoryModule,
ProfileRepositoryModule,
AcademicRepositoryModule,
portalRepositoryModule,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.chukchukhaksa.mobile.domain.portal.model

sealed class PortalScrapingError(val defaultMessage: String) {

data object InvalidCredentials : PortalScrapingError(
"아이디나 비밀번호가 일치하지 않습니다.\n학교 홈페이지에서 확인해주세요.",
)

data object AccountLocked : PortalScrapingError(
"계정이 잠겼습니다. 포털사이트에서 비밀번호 재설정을 진행해주세요.",
)

data object InvalidAcademicRecord : PortalScrapingError(
"입력하신 학적 정보로는 현재 처리가 불가능합니다.\n세부 사유를 확인해주세요.",
)

data object AlreadyConnected : PortalScrapingError(
"이미 포털 연동된 학생 정보가 존재합니다.\n다른 계정으로 로그인했는지 확인해주세요.",
)

data object GraduationDataNotFound : PortalScrapingError(
"사용자에게 맞는 졸업 요건 데이터가 존재하지 않습니다.\n학과/입학년도 정보를 확인해주세요.",
)

data object DoubleMajorInfoMissing : PortalScrapingError(
"복수전공 이수 구분 정보가 존재하지 않아 처리할 수 없습니다.\n학사정보를 확인해주세요.",
)

data object TransferStudentNotSupported : PortalScrapingError(
"편입생 학적 정보는 현재 지원되지 않습니다.\n추후 지원 예정입니다.",
)

data class Unknown(
val httpStatus: Int?,
val appCode: String?,
) : PortalScrapingError(
"알 수 없는 오류가 발생했어요.\n잠시 후 다시 시도해주세요.",
)
Comment on lines +3 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

도메인 계층에 사용자 문구를 고정하지 않는 편이 좋습니다.

defaultMessage를 도메인 모델에 넣으면 i18n/카피 변경 시 도메인까지 수정해야 합니다. 도메인에는 에러 타입/코드만 두고, 메시지 매핑은 프레젠테이션(또는 리소스) 계층으로 분리하는 구조를 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/portal/model/PortalScrapingError.kt`
around lines 3 - 38, The domain model PortalScrapingError currently embeds
user-facing text via the defaultMessage constructor parameter; remove that UI
text by deleting the defaultMessage property from the sealed class and from all
subclasses (InvalidCredentials, AccountLocked, InvalidAcademicRecord,
AlreadyConnected, GraduationDataNotFound, DoubleMajorInfoMissing,
TransferStudentNotSupported, and Unknown) so each variant only represents an
error type and carries domain-relevant data (e.g., Unknown keeps httpStatus and
appCode); then map these error types to localized/user-facing strings in the
presentation/resource layer (not in this domain file). Ensure all call sites
constructing or pattern-matching PortalScrapingError are updated to stop
supplying/expecting defaultMessage and instead use the new presentation-layer
mapper to obtain messages.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chukchukhaksa.mobile.domain.portal.model

class PortalScrapingException(
val error: PortalScrapingError,
val httpStatus: Int?,
val appCode: String?,
override val message: String = error.defaultMessage,
) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.chukchukhaksa.mobile.domain.portal.model

data class ScrapingResult(
val taskId: String?,
val studentInfo: StudentInfo?,
val status: String?,
)

data class StudentInfo(
val name: String?,
val school: String?,
val majorName: String?,
val studentCode: String?,
val gradeLevel: Int?,
val status: String?,
val completedSemesterType: Int?,
)
Comment on lines +3 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

status를 문자열 대신 타입으로 제한하는 것을 권장합니다.

Line 6, Line 15의 String? 상태값은 호출부에서 문자열 비교 분기를 유도해 오타/누락 리스크가 큽니다. 상태가 정해진 값 집합이라면 enum class 또는 sealed 타입으로 올리는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/portal/model/ScrapingResult.kt`
around lines 3 - 17, The two nullable String status fields on
ScrapingResult.status and StudentInfo.status should be replaced with a
strongly-typed status (e.g., an enum or sealed type) to avoid fragile string
comparisons: add a ScrapingStatus enum (or sealed class) enumerating the allowed
states, change ScrapingResult.status: String? -> ScrapingStatus? and
StudentInfo.status: String? -> ScrapingStatus?, and update all
parsers/serializers and call sites to map incoming string values into
ScrapingStatus (handling unknown/null values safely) and to use ScrapingStatus
in comparisons instead of raw strings.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.chukchukhaksa.mobile.domain.portal.repository

import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult

interface PortalRepository {
suspend fun login(username: String, password: String)
suspend fun startScraping(): ScrapingResult
suspend fun refreshScraping(): ScrapingResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.chukchukhaksa.mobile.domain.portal.usecase

import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled
import com.chukchukhaksa.mobile.domain.portal.repository.PortalRepository

class PortalLoginUseCase(
private val portalRepository: PortalRepository,
) {
suspend operator fun invoke(
username: String,
password: String,
): Result<Unit> = runCatchingIgnoreCancelled {
portalRepository.login(username, password)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.chukchukhaksa.mobile.domain.portal.usecase

import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled
import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult
import com.chukchukhaksa.mobile.domain.portal.repository.PortalRepository

class RefreshPortalScrapingUseCase(
private val portalRepository: PortalRepository,
) {
suspend operator fun invoke(): Result<ScrapingResult> = runCatchingIgnoreCancelled {
portalRepository.refreshScraping()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.chukchukhaksa.mobile.domain.portal.usecase

import com.chukchukhaksa.mobile.domain.common.runCatchingIgnoreCancelled
import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult
import com.chukchukhaksa.mobile.domain.portal.repository.PortalRepository

class StartPortalScrapingUseCase(
private val portalRepository: PortalRepository,
) {
suspend operator fun invoke(): Result<ScrapingResult> = runCatchingIgnoreCancelled {
portalRepository.startScraping()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.chukchukhaksa.mobile.remote.portal

import com.chukchukhaksa.mobile.data.portal.datasource.PortalRemoteDataSource
import com.chukchukhaksa.mobile.domain.portal.model.PortalScrapingException
import com.chukchukhaksa.mobile.domain.portal.model.ScrapingResult
import com.chukchukhaksa.mobile.domain.portal.model.StudentInfo
import com.chukchukhaksa.mobile.remote.common.ApiResponse
import com.chukchukhaksa.mobile.remote.portal.model.ScrapingResponseDto
import com.chukchukhaksa.mobile.remote.portal.model.StudentInfoDto
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import kotlinx.serialization.json.JsonObject

class PortalRemoteDataSourceImpl(
private val httpClient: HttpClient,
) : PortalRemoteDataSource {

override suspend fun login(username: String, password: String) {
val httpResponse = httpClient.post("suwon-scrape/login") {
parameter("username", username)
parameter("password", password)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
parseOrThrow<JsonObject>(httpResponse)
}

override suspend fun startScraping(): ScrapingResult {
val httpResponse = httpClient.post("suwon-scrape/start")
val dto = parseOrThrow<ScrapingResponseDto>(httpResponse)
return dto.toDomain()
}

override suspend fun refreshScraping(): ScrapingResult {
val httpResponse = httpClient.post("suwon-scrape/refresh")
val dto = parseOrThrow<ScrapingResponseDto>(httpResponse)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return dto.toDomain()
}

private suspend inline fun <reified T> parseOrThrow(httpResponse: HttpResponse): T {
val status = httpResponse.status.value
val apiResponse = runCatching {
httpResponse.body<ApiResponse<T>>()
}.getOrElse { cause ->
throw PortalScrapingException(
error = mapToPortalScrapingError(httpStatus = status, appCode = null),
httpStatus = status,
appCode = null,
message = cause.message ?: "응답 파싱에 실패했습니다.",
)
}

val data = apiResponse.data
if (apiResponse.success && data != null) {
return data
}

val appCode = apiResponse.error?.code
val error = mapToPortalScrapingError(httpStatus = status, appCode = appCode)
throw PortalScrapingException(
error = error,
httpStatus = status,
appCode = appCode,
message = apiResponse.error?.message ?: error.defaultMessage,
)
}
}

private fun ScrapingResponseDto.toDomain() = ScrapingResult(
taskId = taskId,
studentInfo = studentInfo?.toDomain(),
status = status,
)

private fun StudentInfoDto.toDomain() = StudentInfo(
name = name,
school = school,
majorName = majorName,
studentCode = studentCode,
gradeLevel = gradeLevel,
status = status,
completedSemesterType = completedSemesterType,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.chukchukhaksa.mobile.remote.portal

import com.chukchukhaksa.mobile.domain.portal.model.PortalScrapingError

internal fun mapToPortalScrapingError(
httpStatus: Int?,
appCode: String?,
): PortalScrapingError = when {
appCode == "S04" -> PortalScrapingError.AlreadyConnected
appCode == "D01" -> PortalScrapingError.DoubleMajorInfoMissing
appCode == "T13" -> PortalScrapingError.TransferStudentNotSupported
appCode == "G02" -> PortalScrapingError.GraduationDataNotFound
httpStatus == 401 -> PortalScrapingError.InvalidCredentials
httpStatus == 404 -> PortalScrapingError.GraduationDataNotFound
httpStatus == 409 -> PortalScrapingError.AlreadyConnected
httpStatus == 422 -> PortalScrapingError.InvalidAcademicRecord
httpStatus == 423 -> PortalScrapingError.AccountLocked
else -> PortalScrapingError.Unknown(httpStatus = httpStatus, appCode = appCode)
Comment on lines +9 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

매핑 코드의 하드코딩 리터럴을 상수로 추출하면 유지보수가 쉬워집니다.

현재 appCode/HTTP 코드가 문자열·숫자 리터럴로 분산되어 있어 추후 확장 시 오타 리스크가 있습니다.

🔧 제안 코드
+private const val APP_CODE_ALREADY_CONNECTED = "S04"
+private const val APP_CODE_DOUBLE_MAJOR_INFO_MISSING = "D01"
+private const val APP_CODE_TRANSFER_STUDENT_NOT_SUPPORTED = "T13"
+private const val APP_CODE_GRADUATION_DATA_NOT_FOUND = "G02"
+
+private const val HTTP_UNAUTHORIZED = 401
+private const val HTTP_NOT_FOUND = 404
+private const val HTTP_CONFLICT = 409
+private const val HTTP_UNPROCESSABLE_ENTITY = 422
+private const val HTTP_LOCKED = 423
+
 internal fun mapToPortalScrapingError(
     httpStatus: Int?,
     appCode: String?,
 ): PortalScrapingError = when {
-    appCode == "S04" -> PortalScrapingError.AlreadyConnected
-    appCode == "D01" -> PortalScrapingError.DoubleMajorInfoMissing
-    appCode == "T13" -> PortalScrapingError.TransferStudentNotSupported
-    appCode == "G02" -> PortalScrapingError.GraduationDataNotFound
-    httpStatus == 401 -> PortalScrapingError.InvalidCredentials
-    httpStatus == 404 -> PortalScrapingError.GraduationDataNotFound
-    httpStatus == 409 -> PortalScrapingError.AlreadyConnected
-    httpStatus == 422 -> PortalScrapingError.InvalidAcademicRecord
-    httpStatus == 423 -> PortalScrapingError.AccountLocked
+    appCode == APP_CODE_ALREADY_CONNECTED -> PortalScrapingError.AlreadyConnected
+    appCode == APP_CODE_DOUBLE_MAJOR_INFO_MISSING -> PortalScrapingError.DoubleMajorInfoMissing
+    appCode == APP_CODE_TRANSFER_STUDENT_NOT_SUPPORTED -> PortalScrapingError.TransferStudentNotSupported
+    appCode == APP_CODE_GRADUATION_DATA_NOT_FOUND -> PortalScrapingError.GraduationDataNotFound
+    httpStatus == HTTP_UNAUTHORIZED -> PortalScrapingError.InvalidCredentials
+    httpStatus == HTTP_NOT_FOUND -> PortalScrapingError.GraduationDataNotFound
+    httpStatus == HTTP_CONFLICT -> PortalScrapingError.AlreadyConnected
+    httpStatus == HTTP_UNPROCESSABLE_ENTITY -> PortalScrapingError.InvalidAcademicRecord
+    httpStatus == HTTP_LOCKED -> PortalScrapingError.AccountLocked
     else -> PortalScrapingError.Unknown(httpStatus = httpStatus, appCode = appCode)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/portal/PortalScrapingErrorMapper.kt`
around lines 9 - 18, The mapping in PortalScrapingErrorMapper.kt hardcodes
appCode strings ("S04","D01","T13","G02") and HTTP status numbers
(401,404,409,422,423); extract these literals into named constants (e.g., const
val APP_S04 = "S04", const val HTTP_401 = 401) placed in the same file
(companion object or top-level object) and replace the inline literals in the
when block that returns PortalScrapingError (the branches referencing appCode
and httpStatus) with those constants to reduce typos and ease future
maintenance.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.chukchukhaksa.mobile.remote.portal.model

import kotlinx.serialization.Serializable

@Serializable
data class ScrapingResponseDto(
val taskId: String? = null,
val studentInfo: StudentInfoDto? = null,
val status: String? = null,
)

@Serializable
data class StudentInfoDto(
val name: String? = null,
val school: String? = null,
val majorName: String? = null,
val studentCode: String? = null,
val gradeLevel: Int? = null,
val status: String? = null,
val completedSemesterType: Int? = null,
)
Loading