Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0cc0494
feat: NotificationSettingViewModel 상태 구현
Kangnets Jun 1, 2026
ef7135b
feat: NotificationSettingScreen에 ViewModel 연결 래퍼 추가
Kangnets Jun 1, 2026
a468277
feat: PushNotificationsViewModel 상태 구현
Kangnets Jun 1, 2026
b5c9e45
feat: PushNotificationsScreen에 ViewModel 연결
Kangnets Jun 1, 2026
aa2e43b
feat: MyPageViewModel 구현
Kangnets Jun 1, 2026
84dcc4a
feat: MyPageScreen에 ViewModel 연결
Kangnets Jun 1, 2026
f4bf389
feat: GradeViewModel 구현
Kangnets Jun 1, 2026
f6f7b57
feat: GradeDetailScreen에 ViewModel 연결
Kangnets Jun 1, 2026
b344b4e
feat: MainViewModel 구현
Kangnets Jun 1, 2026
12ebc4f
feat: MainScreen에 ViewModel 연결
Kangnets Jun 1, 2026
cd80a0d
fix: 홈 화면 하단이 탭바에 가려 스크롤 안 되는 문제 수정
Kangnets Jun 1, 2026
e4c158c
fix: 바텀 네비게이션 라벨 하단 잘림 수정
Kangnets Jun 1, 2026
e3be5e3
fix: 채플 헤더에서 뒤로가기 화살표/안내 아이콘 제거
Kangnets Jun 1, 2026
77f9968
chore: 채플 출석 인증하기 버튼 임시 주석 처리
Kangnets Jun 1, 2026
12814fb
fix: 이번 학기 남은 출석 최대 횟수를 7회로 제한
Kangnets Jun 1, 2026
4e6e7c1
feat: ChapelViewModel에 휴학 여부(isOnLeave) 노출
Kangnets Jun 1, 2026
938af2b
feat: 휴학생에게 수강할 채플 없음 안내 표시
Kangnets Jun 1, 2026
54f9c70
style: 채플 타이틀 크기를 알림 탭 기준(22sp)으로 통일
Kangnets Jun 1, 2026
d6ad669
style: 설정 타이틀 크기를 알림 탭 기준(22sp)으로 통일
Kangnets Jun 1, 2026
db90398
feat: ReportCardRepository에 전체 학기 목록(semesters) Flow 추가
Kangnets Jun 1, 2026
a747cd1
fix: 홈 전체 학기 추이를 전체 학기 목록 기반으로 즉시 렌더 + 높이 비례화
Kangnets Jun 1, 2026
8d97eb8
fix: 전체 학기 추이 막대 바닥선 정렬
Kangnets Jun 1, 2026
5c39c53
fix: 성적 상세 탭/추이도 전체 학기 목록 기반으로 즉시 렌더
Kangnets Jun 1, 2026
8af41c0
fix: 성적 상세 강의 제목 길면 말줄임(...) 처리
Kangnets Jun 1, 2026
1935f8d
fix: GpaChartCard fillMaxHeight import 누락 수정
Kangnets Jun 1, 2026
17ce6b8
refactor: MySeatLocationScreen를 public으로 변경
Kangnets Jun 1, 2026
aab0e37
feat: 채플 탭 실제 출석/좌석 데이터 연결 + 좌석 상세 화면 진입
Kangnets Jun 1, 2026
e7852b3
feat: 이번 학기 성적 바텀시트용 SemesterCourseItem 모델 추가
Kangnets Jun 1, 2026
3647c33
feat: MainViewModel에 이번 학기 성적(바텀시트용) 상태 추가
Kangnets Jun 1, 2026
2517d5f
feat: GradeBottomSheet 파라미터화 + 미등록 시 ? 표시
Kangnets Jun 1, 2026
6c84564
feat: 홈 이번 학기 성적보기→바텀시트, 전체 학기 추이 자세히→성적상세
Kangnets Jun 1, 2026
1093436
fix: 전체 학기 추이 '자세히'를 성적 상세로 연결
Kangnets Jun 1, 2026
97303f9
fix: MainScreen에 ExperimentalMaterial3Api opt-in 추가
Kangnets Jun 1, 2026
e1e684c
feat: 홈 추이에서 휴학 학기 제외 + P/F 학기 성적 유지 + 휴학 플래그
Kangnets Jun 1, 2026
1ebf0f8
feat: 성적 추이에서 휴학 학기 제외 + P/F 학기 성적 유지
Kangnets Jun 1, 2026
2aa209c
feat: 이번 학기 성적 바텀시트에 휴학 안내 추가
Kangnets Jun 1, 2026
70e7f1c
feat: 홈 바텀시트에 휴학 여부 전달
Kangnets Jun 1, 2026
c35b35e
fix: 이번 학기 성적 바텀시트가 현재 학기(날짜 기준)를 표시하도록 수정
Kangnets Jun 1, 2026
4952f72
feat: GradeUiState에 선택 학기 성적 공개 여부(registered) 추가
Kangnets Jun 1, 2026
dfc9ec6
feat: 성적 상세에서 미공개 학기는 '아직 등록된 성적이 없어요' 표시
Kangnets Jun 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.yourssu.soomsil.usaint.data.source.local.dao.SemesterDao
import com.yourssu.soomsil.usaint.data.source.local.datastore.ReportCardSummaryDataSource
import com.yourssu.soomsil.usaint.data.source.local.datastore.StudentCredentialDataSource
import com.yourssu.soomsil.usaint.data.source.local.entity.LectureEntity
import com.yourssu.soomsil.usaint.data.source.local.entity.SemesterEntity
import com.yourssu.soomsil.usaint.data.source.local.entity.asEntity
import com.yourssu.soomsil.usaint.data.source.local.entity.asExternalModel
import com.yourssu.soomsil.usaint.data.source.remote.USaintRemoteSource
Expand Down Expand Up @@ -35,6 +36,13 @@ class ReportCardRepository @Inject constructor(
.mapValues { (_, lectureEntity) -> lectureEntity.map(LectureEntity::asExternalModel) }
}

// 전체 학기 목록(학기별 GPA 포함). Semester 테이블은 한 번에 채워지므로
// 강의 적재를 기다리는 semesterWithLectures와 달리 즉시 전체가 로드된다.
val semesters: Flow<List<SemesterData>> =
semesterDao.getSemesterEntities().map { entities ->
entities.map(SemesterEntity::asExternalModel)
}

suspend fun fetchReportCardSummary(): Result<Unit> = runCatching {
val credential = studentCredential.getStudentCredential()
val reportCardSummaryData = uSaintRemoteSource.remoteReportCardSummaryData(credential)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -49,10 +54,43 @@ fun ChapelScreen(
// onRefresh = { viewModel.fetchData(refresh = true) },
// onPasswordChange = viewModel::changePassword
// )
ChapelSeatScreen(
modifier = modifier
)

val isOnLeave by viewModel.isOnLeave.collectAsStateWithLifecycle()
val chapelUiState by viewModel.chapelUiState.collectAsStateWithLifecycle()
var showSeatLocation by remember { mutableStateOf(false) }

val chapelCard = (chapelUiState as? ChapelUiState.Chapel)?.chapelCard
val simple = chapelCard?.chapelSimpleData
val attendances = chapelCard?.chapelAttendances.orEmpty()
// 실제 내 출석/지각 일수 집계
val attended = attendances.count { it.attendance == "출석" }
val late = attendances.count { it.attendance == "지각" }

val seatNumber = simple?.seatNumber?.takeIf { it.isNotBlank() } ?: "-"
val seatFloor = simple?.let { "${it.floorLevel}층" } ?: ""
val seatZone = simple?.chapelRoom ?: ""

if (showSeatLocation) {
MySeatLocationScreen(
seatCode = seatNumber,
seatFloor = seatFloor,
seatBuilding = seatZone,
onBackClick = { showSeatLocation = false },
modifier = modifier,
)
} else {
ChapelSeatScreen(
seatNumber = seatNumber,
seatFloor = seatFloor,
seatZone = seatZone,
attended = attended,
total = MAX_REQUIRED_CHAPEL,
late = late,
progress = (attended.toFloat() / MAX_REQUIRED_CHAPEL).coerceIn(0f, 1f),
isOnLeave = isOnLeave,
onViewSeatClick = { showSeatLocation = true },
modifier = modifier,
)
}
}
/*
@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -298,36 +336,14 @@ fun ChapelHeader(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_caret_left),
contentDescription = "뒤로가기",
modifier = Modifier
.size(24.dp)
.clickable { onBackClick() },
tint = Color(0xFF0A0A0A)
)
Text(
text = "채플",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF0A0A0A),
letterSpacing = (-0.3).sp
)
}
Icon(
painter = painterResource(R.drawable.ic_info),
contentDescription = "정보",
modifier = Modifier
.size(22.dp)
.clickable { onInfoClick() },
tint = Color(0xFF9CA3AF)
Text(
text = "채플",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF0A0A0A),
letterSpacing = (-0.5).sp
)
}
}
Expand Down Expand Up @@ -391,6 +407,9 @@ fun SeatHeroCard(

// ─── Attendance Gauge ───

// 한 학기 채플 이수 횟수는 최대 7회
private const val MAX_REQUIRED_CHAPEL = 7

@Composable
fun AttendanceGauge(
attended: Int,
Expand All @@ -399,6 +418,8 @@ fun AttendanceGauge(
progress: Float,
modifier: Modifier = Modifier
) {
val cappedTotal = total.coerceAtMost(MAX_REQUIRED_CHAPEL)
val remaining = (cappedTotal - attended).coerceAtLeast(0)
Column(
modifier = modifier
.fillMaxWidth()
Expand All @@ -425,14 +446,14 @@ fun AttendanceGauge(
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
Text(
text = "${total - attended}",
text = "$remaining",
fontSize = 18.sp,
fontWeight = FontWeight.ExtraBold,
color = Color(0xFF0062FF),
letterSpacing = (-0.4).sp
)
Text(
text = "/ ${total}회",
text = "/ ${cappedTotal}회",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF9CA3AF)
Expand Down Expand Up @@ -538,13 +559,14 @@ fun ChapelSeatScreen(
seatFloor: String = "1층 앞자리",
seatZone: String = "A구역",
attended: Int = 5,
total: Int = 8,
total: Int = 7,
late: Int = 1,
progress: Float = 0.6f,
progress: Float = 0.71f,
onBackClick: () -> Unit = {},
onInfoClick: () -> Unit = {},
onViewSeatClick: () -> Unit = {},
onAttendClick: () -> Unit = {},
isOnLeave: Boolean = false,
modifier: Modifier = Modifier
) {
Column(
Expand All @@ -557,26 +579,63 @@ fun ChapelSeatScreen(
onInfoClick = onInfoClick
)

Column(
modifier = Modifier
.weight(1f)
.padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
SeatHeroCard(
seatNumber = seatNumber,
floor = seatFloor,
zone = seatZone,
onViewSeatClick = onViewSeatClick
)
AttendanceGauge(
attended = attended,
total = total,
late = late,
progress = progress
if (isOnLeave) {
ChapelOnLeaveMessage(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
)
} else {
Column(
modifier = Modifier
.weight(1f)
.padding(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
SeatHeroCard(
seatNumber = seatNumber,
floor = seatFloor,
zone = seatZone,
onViewSeatClick = onViewSeatClick
)
AttendanceGauge(
attended = attended,
total = total,
late = late,
progress = progress
)
}

// TODO: 출석 인증(QR) 기능 구현 전까지 임시 비활성화
// AttendanceCta(onClick = onAttendClick)
}
}
}

AttendanceCta(onClick = onAttendClick)
// ─── 휴학 안내 ───

@Composable
private fun ChapelOnLeaveMessage(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.padding(horizontal = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
Text(
text = "휴학 중이에요",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF0A0A0A),
letterSpacing = (-0.3).sp
)
Text(
text = "휴학 중이라 수강할 채플이 없어요",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF9CA3AF),
letterSpacing = (-0.2).sp
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ private fun HelperText(
// ─── Screen ───

@Composable
private fun MySeatLocationScreen(
fun MySeatLocationScreen(
seatCode: String = "B-12",
seatFloor: String = "1층 앞자리",
seatBuilding: String = "한경직기념관",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yourssu.soomsil.usaint.core.model.ChapelData
import com.yourssu.soomsil.usaint.data.repository.ChapelRepository
import com.yourssu.soomsil.usaint.data.repository.StudentDataRepository
import com.yourssu.soomsil.usaint.data.source.local.datastore.StudentCredentialDataSource
import com.yourssu.soomsil.usaint.domain.usecase.GetCurrentSemesterUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
Expand All @@ -36,9 +38,19 @@ class ChapelViewModel @Inject constructor(
private val studentCredential: StudentCredentialDataSource,
private val chapelRepository: ChapelRepository,
private val getCurrentSemesterUseCase: GetCurrentSemesterUseCase,
studentDataRepository: StudentDataRepository,
) : ViewModel() {
private var showPasswordIncorrectSnackbar = mutableStateOf(false)

// 휴학 중인 학생은 수강할 채플이 없음 (학적 상태로 판별)
val isOnLeave: StateFlow<Boolean> = studentDataRepository.studentData
.map { it.status.contains("휴학") }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false,
)

val chapelUiState: StateFlow<ChapelUiState> = combine(
chapelRepository.chapelCard,
chapelRepository.chapels,
Expand Down
Loading