Skip to content

Comments

Feat/#5 mypage screen구현#41

Merged
starshape7 merged 4 commits intodevelopfrom
feat/#5-mypage-screen구현
Jan 29, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/#5-mypage-screen\uad6c\ud604"
Merged

Feat/#5 mypage screen구현#41
starshape7 merged 4 commits intodevelopfrom
feat/#5-mypage-screen구현

Conversation

@starshape7
Copy link
Collaborator

@starshape7 starshape7 commented Jan 27, 2026

Related issue 🛠

Work Description 📝

  • 대기 중 스터디 빈 경우 알림 문구 수정
  • 모집중 스터디 승인/거절 처리 구현

Screenshot 📸

Uncompleted Tasks 😅

PR Point 📌

트러블 슈팅 💥

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 스터디 신청자 관리 기능 추가: 신청자 목록 조회 및 신청 수락/거절 가능
    • 신청 확인 화면 추가: 닉네임, 자기소개, 프로필 이미지 등 신청자 정보 표시
    • 네비게이션 연동: 특정 스터디의 신청 확인 화면으로 이동 가능
  • 버그 수정 / 개선

    • 신청 수락/거절 확인 대화상자 추가로 실수 방지
    • 화면 제목 및 일부 안내 문구(대기 화면) 개선 (신청 확인 표기 반영)

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

Walkthrough

스터디 지원자 조회 및 수락/거절 기능을 전체 계층(서비스, 데이터소스, 리포지토리, 도메인, 매퍼, ViewModel, Compose UI, 네비게이션)에 걸쳐 추가했습니다. 새로운 API 엔드포인트 및 화면/네비게이션이 포함됩니다. (47자)

Changes

Cohort / File(s) 변경 사항
도메인 모델
domain/study/src/main/java/com/umcspot/spot/study/model/StudyApplicationResultList.kt
StudyApplicationResultList, StudyApplicationResult 데이터 클래스 추가
저장소 인터페이스
domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt
getStudyApplications(studyId: Long), entryAcceptance(applicationId: Long, decision: String) 메서드 추가
데이터 DTO
data/study/src/main/java/com/umcspot/spot/study/dto/response/StudyApplicationResponseDto.kt
StudyApplicationResponseDto, StudyApplication DTO 추가 (@Serializable, @SerialName 매핑)
데이터 소스 & 구현
data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt, data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt
getStudyApplications()entryAcceptance() 시그니처 추가 및 구현(서비스로 위임)
API 서비스
data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt
GET /api/studies/{studyId}/applications 및 POST /api/studies/applications/{applicationId} 엔드포인트 추가
매퍼
data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt
DTO → 도메인 변환 추가: StudyApplicationResponseDto.toDomainList(), StudyApplication.toDomain()
저장소 구현
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
getStudyApplications(), entryAcceptance() 구현(에러 처리 및 매핑 포함)
모집 스터디 화면
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/RecruitingStudyScreen.kt
moveToCheckApplied 콜백 시그니처 변경: () -> Unit(Long) -> Unit
지원자 확인 ViewModel & 상태
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt, ...State.kt
RecruitingStudyApplicationViewModel(load/accept/reject), RecruitingStudyApplicationState 추가
지원자 확인 UI
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt
RecruitingStudyRequestScreen, RecruitingStudyScreenContent, ApplicationItem 컴포저블 추가
지원자 확인 네비게이션
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/navigation/RecruitingStudyApplicationNavigation.kt
navigateToStudyApplications(studyId: Long), recruitingStudyApplicationsGraph(...), STUDY_APPLICATION_ROUTE 추가
모집 네비게이션
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/navigation/RecruitingStudyNavigation.kt
myRecruitingStudyGraphmoveToCheckApplied 파라미터 타입 변경 전파
메인 네비게이션/화면
feature/main/src/main/java/com/umcspot/spot/main/*.kt
STUDY_APPLICATION_ROUTE 도입, 라우팅/상단바/탑FAB 표시 조건에 해당 라우트 추가
기타 UI
feature/mypage/src/main/java/com/umcspot/spot/mypage/waiting/WaitingStudyScreen.kt
빈 상태 안내 문구 텍스트 수정

Sequence Diagram(s)

sequenceDiagram
    actor User as 사용자
    participant UI as RecruitingStudyRequestScreen
    participant VM as RecruitingStudyApplicationViewModel
    participant Repo as StudyRepository
    participant DS as StudyDataSource
    participant API as StudyService (API)

    User->>UI: 화면 진입 (studyId)
    UI->>VM: load(studyId)
    VM->>Repo: getStudyApplications(studyId)
    Repo->>DS: getStudyApplications(studyId)
    DS->>API: GET /api/studies/{studyId}/applications
    API-->>DS: BaseResponse<StudyApplicationResponseDto>
    DS-->>Repo: DTO 반환
    Repo->>Repo: DTO → 도메인 매핑
    Repo-->>VM: Result<StudyApplicationResultList>
    VM-->>UI: uiState 업데이트 (Success)

    User->>UI: 수락/거절 버튼 클릭 (applicationId)
    UI->>VM: accept(applicationId) / reject(applicationId)
    VM->>Repo: entryAcceptance(applicationId, decision)
    Repo->>DS: entryAcceptance(applicationId, decision)
    DS->>API: POST /api/studies/applications/{applicationId}?decision=...
    API-->>DS: NullResultResponse
    DS-->>Repo: 성공 응답
    VM->>Repo: getStudyApplications(currentStudyId) (재로딩)
    Repo-->>VM: 갱신된 목록
    VM-->>UI: uiState 갱신
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

🐶 종훈, 🍒 [FEAT]

Poem

🐰 지원자 명단을 맞이했어요,
버튼 한 번에 수락도 거절도 척척,
네비게이션 타고 화면에 날아와,
ViewModel이 데이터를 춤추게 하네,
작은 당근으로 축하를! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 변경사항을 명확하게 설명하고 있습니다. '모집중 스터디 승인/거절 처리 구현'과 '대기 중 스터디 빈 경우 알림 문구 수정' 등 주요 작업이 설명되어 있습니다.
Description check ✅ Passed PR 설명이 필수 섹션의 대부분을 포함하고 있으나, 관련 이슈 번호가 기입되지 않았으며 스크린샷 src 값이 비어있고 PR Point와 트러블슈팅 섹션이 작성되지 않았습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/navigation/RecruitingStudyApplicationNavigation.kt`:
- Around line 33-34: `@Serializable` is incorrectly applied to the string
constant STUDY_APPLICATION_ROUTE; remove the `@Serializable` annotation from the
const val declaration and also delete the unused import
`kotlinx.serialization.Serializable` (the annotation belongs on classes/data
classes, not string constants). Ensure only the const val
STUDY_APPLICATION_ROUTE = "studyApplication/{studyId}" remains and no leftover
import or annotation references exist.
- Around line 22-23: backStackEntry.arguments?.getLong("studyId") returns 0L
when the key is missing so the current defensive ?: return@composable won't run;
change the logic to first ensure arguments is non-null and contains the
"studyId" key before calling getLong. For example, inside the composable guard
on backStackEntry.arguments (or assign val args = backStackEntry.arguments ?:
return@composable) then check args.containsKey("studyId") ?: return@composable
and only then call val studyId = args.getLong("studyId"); update the code paths
referencing backStackEntry, studyId and the composable accordingly.

In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt`:
- Around line 155-170: The AcceptDialog currently shows a "수락 완료" message but
triggers viewmodel.accept(selectedApplicationId!!) only when the "확인" button is
pressed, which is confusing; change the dialog to a confirmation flow by
updating AcceptDialog props (e.g., modalTitle -> "수락하시겠어요?", modalDes ->
confirmation text) and keep the API call in onClick to perform accept, then on
successful completion show a separate success feedback (either open a new
AcceptDialog/SuccessDialog or trigger a toast) — locate the AcceptDialog usage,
the showAcceptDialog boolean, viewmodel.accept(...) and selectedApplicationId to
implement this: modify the dialog copy to be a confirmation prompt, call
viewmodel.accept on confirm, and handle success by setting a new state (e.g.,
showAcceptSuccessDialog or showToast) to display the completed message after the
API returns.
- Line 24: Remove the unused import of androidx.compose.material3.TextButton in
RecruitingStudyApplicationScreen.kt since the file uses the custom TextButton
instead; delete the line importing androidx.compose.material3.TextButton so only
the project's custom TextButton import remains (ensure no references to
material3.TextButton exist in the RecruitingStudyApplicationScreen composable).
- Around line 146-152: The code currently force-unwraps selectedApplicationId
(selectedApplicationId!!) in the Reject dialog onClick handler which can cause
NPE; modify the handler to defensively check for null before calling
viewmodel.reject — e.g., use a safe-call/let pattern (selectedApplicationId?.let
{ viewmodel.reject(it) } ) or an early return so the dialog is dismissed only
after a valid id is present; apply the same change to the corresponding
AcceptDialog handler (the similar force-unwrapping at the AcceptDialog ~168
reference) to avoid the same NPE.

In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt`:
- Around line 31-46: Move the side-effect assignment of currentStudyId out of
the _uiState.update lambda so update stays a pure state transformation: assign
currentStudyId = studyId before calling _uiState.update, then call
_uiState.update { it.copy(applications = if (info.applies.isEmpty())
UiState.Empty else UiState.Success(info)) } inside onSuccess. In onFailure,
replace the lone Log.e call with a UI state update that sets applications =
UiState.Failure(e.message ?: "신청 목록 불러오기 실패") (use the same _uiState.update
pattern) so the user sees an error, and remove the commented-out weather-related
code; ensure you reference the existing symbols _uiState.update, currentStudyId,
applications, UiState.Success, UiState.Empty, UiState.Failure, and onFailure
while making these changes.
🧹 Nitpick comments (5)
feature/mypage/src/main/java/com/umcspot/spot/mypage/waiting/WaitingStudyScreen.kt (2)

146-173: indexOf 대신 itemsIndexed 사용을 권장합니다.

studyList.indexOf(item)은 매 아이템마다 O(n) 선형 탐색을 수행하여 전체 O(n²) 복잡도가 됩니다. 페이지네이션으로 데이터가 누적될 경우 성능에 영향을 줄 수 있습니다.

♻️ 제안된 수정
-        items(
-            items = studyList,
-            key = { it.id }
-        ) { item ->
+        itemsIndexed(
+            items = studyList,
+            key = { _, item -> item.id }
+        ) { index, item ->
             Spacer(Modifier.height(screenHeightDp(5.dp)))

             Box(
                 modifier = Modifier.fillMaxWidth(),
                 contentAlignment = Alignment.TopStart
             ) {
                 StudyListItem(
                     item = item,
                     modifier = Modifier.fillMaxWidth(),
                     onClick = { onStudyClick(item.id) }
                 )
             }

-            if (studyList.indexOf(item) != studyList.lastIndex) {
+            if (index != studyList.lastIndex) {
                 Spacer(Modifier.height(screenHeightDp(5.dp)))

                 HorizontalDivider(
                     modifier = Modifier
                         .fillMaxWidth(),
                     color = SpotTheme.colors.G300,
                     thickness = 1.dp
                 )
             }
         }

120-131: 테마 색상 사용 불일치.

Line 98에서는 SpotTheme.colors.white를 사용하지만, Line 124에서는 Color.White를 직접 사용하고 있습니다. 테마 지원(다크 모드 등)을 위해 일관되게 테마 색상을 사용하는 것이 좋습니다.

♻️ 제안된 수정
         is UiState.Success -> {
             WaitingStudyScreenContent(
                 modifier = Modifier
                     .fillMaxSize()
-                    .background(Color.White)
+                    .background(SpotTheme.colors.white)
                     .padding(top = contentPadding.calculateTopPadding(), bottom = contentPadding.calculateBottomPadding())
                     .padding(horizontal = screenWidthDp(17.dp)),
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt (1)

211-217: entryAcceptance에 에러 로깅 및 응답 검증 누락

다른 메서드들과 달리 entryAcceptance에는 onFailure 에러 로깅이 없고, 응답의 성공 여부 검증도 없습니다. NullResultResponseisSuccess 플래그를 포함한다면, 이를 검증하는 것이 좋습니다.

♻️ 일관성 있는 에러 처리 제안
     override suspend fun entryAcceptance(
         applicationId: Long,
         decision: String
     ): Result<Unit> =
         runCatching {
-            studyDataSource.entryAcceptance(applicationId, decision)
-        }
+            val response = studyDataSource.entryAcceptance(applicationId, decision)
+            if (!response.isSuccess) {
+                throw Exception(response.message ?: "신청 처리 실패")
+            }
+        }.onFailure {
+            Log.e("StudyRepository", "entryAcceptance failed", it)
+        }
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt (1)

50-72: 수락/거절 실패 시 사용자 피드백 없음

accept()reject() 함수에서 onFailure 시 로깅만 수행하고 사용자에게 에러를 알리지 않습니다. 네트워크 오류나 서버 에러 발생 시 사용자는 아무런 피드백 없이 대기하게 됩니다.

에러 발생 시 토스트 메시지나 스낵바를 통해 사용자에게 알려주는 것을 권장합니다. SingleEvent 또는 SharedFlow를 사용하여 일회성 이벤트를 처리할 수 있습니다.

feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt (1)

204-204: indexOf() 호출로 인한 O(n²) 복잡도

items 루프 내에서 applicationList.indexOf(item)을 호출하면 매 아이템마다 리스트를 순회하여 비효율적입니다. itemsIndexed를 사용하면 인덱스를 직접 얻을 수 있습니다.

♻️ 수정 제안
-        items(
-            items = applicationList,
-            key = { it.applicantId }
-        ) { item ->
+        itemsIndexed(
+            items = applicationList,
+            key = { _, item -> item.applicantId }
+        ) { index, item ->
             Spacer(Modifier.height(screenHeightDp(5.dp)))
             ...
-            if (applicationList.indexOf(item) != applicationList.lastIndex) {
+            if (index != applicationList.lastIndex) {

items 대신 itemsIndexed import 추가:

import androidx.compose.foundation.lazy.itemsIndexed

Comment on lines +22 to +23
) { backStackEntry ->
val studyId = backStackEntry.arguments?.getLong("studyId") ?: return@composable
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getLong은 키가 없을 때 0L을 반환하므로 의도한 방어 로직이 동작하지 않습니다.

Bundle.getLong("studyId")는 키가 존재하지 않을 때 null이 아닌 0L을 반환합니다. 따라서 ?: return@composable은 실행되지 않고, 잘못된 studyId = 0L로 화면이 로드될 수 있습니다.

🐛 수정 제안
-        val studyId = backStackEntry.arguments?.getLong("studyId") ?: return@composable
+        val studyId = backStackEntry.arguments?.getLong("studyId")
+            .takeIf { it != 0L } ?: return@composable

또는 argumentsnull인지 먼저 확인:

-        val studyId = backStackEntry.arguments?.getLong("studyId") ?: return@composable
+        val args = backStackEntry.arguments ?: return@composable
+        val studyId = args.getLong("studyId")
🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/navigation/RecruitingStudyApplicationNavigation.kt`
around lines 22 - 23, backStackEntry.arguments?.getLong("studyId") returns 0L
when the key is missing so the current defensive ?: return@composable won't run;
change the logic to first ensure arguments is non-null and contains the
"studyId" key before calling getLong. For example, inside the composable guard
on backStackEntry.arguments (or assign val args = backStackEntry.arguments ?:
return@composable) then check args.containsKey("studyId") ?: return@composable
and only then call val studyId = args.getLong("studyId"); update the code paths
referencing backStackEntry, studyId and the composable accordingly.

Comment on lines +33 to +34
@Serializable
const val STUDY_APPLICATION_ROUTE = "studyApplication/{studyId}"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

@Serializable 어노테이션이 const val에 잘못 적용되었습니다.

@Serializable은 클래스나 데이터 클래스에 사용되며, 문자열 상수에는 적용되지 않습니다. 이 어노테이션은 불필요하며 컴파일 경고를 발생시킬 수 있습니다.

♻️ 수정 제안
-@Serializable
 const val STUDY_APPLICATION_ROUTE = "studyApplication/{studyId}"

또한 10번 라인의 import kotlinx.serialization.Serializable도 제거하세요.

🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/navigation/RecruitingStudyApplicationNavigation.kt`
around lines 33 - 34, `@Serializable` is incorrectly applied to the string
constant STUDY_APPLICATION_ROUTE; remove the `@Serializable` annotation from the
const val declaration and also delete the unused import
`kotlinx.serialization.Serializable` (the annotation belongs on classes/data
classes, not string constants). Ensure only the const val
STUDY_APPLICATION_ROUTE = "studyApplication/{studyId}" remains and no leftover
import or annotation references exist.

Comment on lines 155 to 170
AcceptDialog(
visible = showAcceptDialog,
painter = painterResource(R.drawable.group_accept),
painterTint = SpotTheme.colors.B500,
modalTitle = "수락 완료",
modalDes = "수락이 완료되었어요.",
okButtonText = "확인",
noButtonText = null,
onDismiss = {
showAcceptDialog = false
},
onClick = {
showAcceptDialog = false
viewmodel.accept(selectedApplicationId!!)
}
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

AcceptDialog의 UX 흐름이 혼란스럽습니다.

다이얼로그 제목이 "수락 완료"이고 설명이 "수락이 완료되었어요"인데, 실제 수락 API 호출은 "확인" 버튼 클릭 시 발생합니다. 사용자는 이미 완료된 것으로 오해할 수 있습니다.

수락 확인을 요청하는 다이얼로그("수락하시겠어요?")를 먼저 보여주고, API 성공 후 완료 메시지를 표시하거나, 즉시 수락 처리 후 결과를 토스트로 알리는 방식을 권장합니다.

🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt`
around lines 155 - 170, The AcceptDialog currently shows a "수락 완료" message but
triggers viewmodel.accept(selectedApplicationId!!) only when the "확인" button is
pressed, which is confusing; change the dialog to a confirmation flow by
updating AcceptDialog props (e.g., modalTitle -> "수락하시겠어요?", modalDes ->
confirmation text) and keep the API call in onClick to perform accept, then on
successful completion show a separate success feedback (either open a new
AcceptDialog/SuccessDialog or trigger a toast) — locate the AcceptDialog usage,
the showAcceptDialog boolean, viewmodel.accept(...) and selectedApplicationId to
implement this: modify the dialog copy to be a confirmation prompt, call
viewmodel.accept on confirm, and handle success by setting a new state (e.g.,
showAcceptSuccessDialog or showToast) to display the completed message after the
API returns.

Comment on lines 31 to 46
.onSuccess { info ->
_uiState.update {
currentStudyId = studyId
it.copy(
applications = if (info.applies.isEmpty()) {
UiState.Empty
} else {
UiState.Success(info)
}
)
}
}
.onFailure { e ->
Log.e("RecruitingStudyApplicationViewModel", "loadStudyApplicationsError", e)
// _uiState.update { it.copy(weatherInfo = UiState.Failure(e.message ?: "날씨 불러오기 실패")) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

_uiState.update 블록 내부에서 side effect 발생 및 에러 처리 미흡

  1. currentStudyId = studyId 할당이 update 람다 내부에 있습니다. update는 순수 함수여야 하며, side effect는 외부에서 처리해야 합니다.
  2. onFailure에서 로깅만 하고 UI 상태를 업데이트하지 않아 사용자에게 에러 피드백이 없습니다.
  3. 45번 라인의 주석 처리된 코드는 정리가 필요합니다.
♻️ 수정 제안
     viewModelScope.launch {
+        currentStudyId = studyId
         studyRepository.getStudyApplications(
             studyId = studyId
         )
         .onSuccess { info ->
             _uiState.update {
-                currentStudyId = studyId
                 it.copy(
                     applications = if (info.applies.isEmpty()) {
                         UiState.Empty
                     } else {
                         UiState.Success(info)
                     }
                 )
             }
         }
         .onFailure { e ->
             Log.e("RecruitingStudyApplicationViewModel", "loadStudyApplicationsError", e)
-//                    _uiState.update { it.copy(weatherInfo = UiState.Failure(e.message ?: "날씨 불러오기 실패")) }
+            _uiState.update { it.copy(applications = UiState.Failure(e.message ?: "데이터를 불러오지 못했습니다")) }
         }
     }
🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt`
around lines 31 - 46, Move the side-effect assignment of currentStudyId out of
the _uiState.update lambda so update stays a pure state transformation: assign
currentStudyId = studyId before calling _uiState.update, then call
_uiState.update { it.copy(applications = if (info.applies.isEmpty())
UiState.Empty else UiState.Success(info)) } inside onSuccess. In onFailure,
replace the lone Log.e call with a UI state update that sets applications =
UiState.Failure(e.message ?: "신청 목록 불러오기 실패") (use the same _uiState.update
pattern) so the user sees an error, and remove the commented-out weather-related
code; ensure you reference the existing symbols _uiState.update, currentStudyId,
applications, UiState.Success, UiState.Empty, UiState.Failure, and onFailure
while making these changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt`:
- Around line 121-124: The current onAcceptClick handler sets showAcceptDialog =
true immediately after calling viewmodel.accept(it), which shows the success
dialog regardless of API outcome; change the flow so the dialog is shown only
when the ViewModel reports success — remove the immediate showAcceptDialog
assignment in RecruitingStudyApplicationScreen's onAcceptClick/onRejectClick and
instead subscribe to the ViewModel's result event (e.g., actionResult SharedFlow
or an equivalent LiveData) inside LaunchedEffect or a collector, then set
showAcceptDialog = true only when the collected result is ActionResult.Success
(and handle ActionResult.Error for toasts); apply the same pattern for the
similar block at the other occurrence.

In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt`:
- Around line 49-71: Add a user-feedback result event and emit success/error
from both accept and reject: introduce a sealed ActionResult (Success /
Error(message)), back it with a private MutableSharedFlow<ActionResult> named
_actionResult and expose actionResult: SharedFlow<ActionResult>; then in
RecruitingStudyApplicationViewModel functions accept(applicationId: Long) and
reject(applicationId: Long) call studyRepository.entryAcceptance(...).onSuccess
{ load(currentStudyId); _actionResult.emit(ActionResult.Success) } and
.onFailure { Log.e(...); _actionResult.emit(ActionResult.Error(e.message ?: "처리에
실패했습니다")) } so the UI can show a toast/snackbar from actionResult.
🧹 Nitpick comments (2)
feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt (2)

182-209: indexOf 대신 itemsIndexed 사용 권장

applicationList.indexOf(item)은 매 항목마다 O(n) 탐색을 수행하여 전체 O(n²) 복잡도가 됩니다. itemsIndexed를 사용하면 인덱스가 직접 제공됩니다.

♻️ 수정 제안
-        items(
-            items = applicationList,
-            key = { it.applicantId }
-        ) { item ->
+        itemsIndexed(
+            items = applicationList,
+            key = { _, item -> item.applicantId }
+        ) { index, item ->
             Spacer(Modifier.height(screenHeightDp(5.dp)))

             Box(
                 modifier = Modifier.fillMaxWidth(),
                 contentAlignment = Alignment.TopStart
             ) {
                 ApplicationItem(
                     applicationInfo = item,
                     onAcceptClick = onAcceptClick,
                     onRejectClick = onRejectClick
                 )
             }

-            if (applicationList.indexOf(item) != applicationList.lastIndex) {
+            if (index != applicationList.lastIndex) {
                 Spacer(Modifier.height(screenHeightDp(5.dp)))

                 HorizontalDivider(
                     modifier = Modifier
                         .fillMaxWidth(),
                     color = SpotTheme.colors.G300,
                     thickness = 1.dp
                 )
             }
         }

98-111: EmptyFailure 상태 구분 필요

UiState.EmptyUiState.Failure를 동일하게 처리하여 사용자가 데이터 없음과 네트워크 오류를 구분할 수 없습니다. 에러 발생 시 적절한 메시지와 재시도 옵션을 제공하는 것이 좋습니다.

♻️ 상태 분리 예시
-        is UiState.Empty, is UiState.Failure -> {
+        is UiState.Empty -> {
             Box(
                 modifier = Modifier
                     .fillMaxSize()
                     .background(SpotTheme.colors.white),
                 contentAlignment = Alignment.Center
             ) {
                 EmptyAlert(
                     alertTitle = "신청자가 아직 없어요!",
                     alertDes = "SPOT과 함께 새로운 만남을 기다려봐요",
                     painter = painterResource(R.drawable.group_add),
                 )
             }
         }
+        is UiState.Failure -> {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(SpotTheme.colors.white),
+                contentAlignment = Alignment.Center
+            ) {
+                EmptyAlert(
+                    alertTitle = "오류가 발생했어요",
+                    alertDes = ui.message ?: "잠시 후 다시 시도해주세요",
+                    painter = painterResource(R.drawable.group_add),
+                    // 재시도 버튼 추가 고려
+                )
+            }
+        }

Comment on lines +121 to +124
onAcceptClick = {
viewmodel.accept(it)
showAcceptDialog = true
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

accept API 결과와 무관하게 성공 다이얼로그 표시

onAcceptClick에서 viewmodel.accept(it)을 호출한 직후 showAcceptDialog = true로 설정합니다. API 호출이 비동기로 실행되므로 실패하더라도 "수락 완료" 다이얼로그가 표시됩니다.

API 성공 콜백에서 다이얼로그를 표시하거나, ViewModel에서 결과 이벤트를 발행하여 UI에서 수신하는 방식을 권장합니다.

🛠️ 수정 접근 방식
 onAcceptClick = {
-    viewmodel.accept(it)
-    showAcceptDialog = true
+    selectedApplicationId = it
+    viewmodel.accept(it)
+    // ViewModel에서 성공 이벤트 발행 시 다이얼로그 표시
 },

ViewModel에서 actionResult SharedFlow를 구독하여 성공 시에만 다이얼로그를 표시:

LaunchedEffect(Unit) {
    viewmodel.actionResult.collect { result ->
        when (result) {
            is ActionResult.Success -> showAcceptDialog = true
            is ActionResult.Error -> { /* 에러 토스트 표시 */ }
        }
    }
}

Also applies to: 151-165

🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationScreen.kt`
around lines 121 - 124, The current onAcceptClick handler sets showAcceptDialog
= true immediately after calling viewmodel.accept(it), which shows the success
dialog regardless of API outcome; change the flow so the dialog is shown only
when the ViewModel reports success — remove the immediate showAcceptDialog
assignment in RecruitingStudyApplicationScreen's onAcceptClick/onRejectClick and
instead subscribe to the ViewModel's result event (e.g., actionResult SharedFlow
or an equivalent LiveData) inside LaunchedEffect or a collector, then set
showAcceptDialog = true only when the collected result is ActionResult.Success
(and handle ActionResult.Error for toasts); apply the same pattern for the
similar block at the other occurrence.

Comment on lines +49 to +71
fun accept(applicationId: Long) {
viewModelScope.launch {
studyRepository.entryAcceptance(applicationId = applicationId, decision = "APPROVE")
.onSuccess {
load(currentStudyId)
}
.onFailure { e ->
Log.e("RecruitingStudyApplicationViewModel", "acceptError", e)
}
}
}

fun reject(applicationId: Long) {
viewModelScope.launch {
studyRepository.entryAcceptance(applicationId = applicationId, decision = "REJECT")
.onSuccess {
load(currentStudyId)
}
.onFailure { e ->
Log.e("RecruitingStudyApplicationViewModel", "rejectError", e)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

accept/reject 실패 시 사용자 피드백 누락

acceptreject 함수 모두 onFailure에서 로깅만 수행합니다. API 호출 실패 시 사용자에게 피드백이 없어 작업 결과를 알 수 없습니다.

에러 상태를 UI 상태에 추가하거나, 결과 이벤트(예: SharedFlow)를 통해 토스트/스낵바로 알림을 표시하는 것을 권장합니다.

🛠️ 결과 이벤트 추가 예시
// State에 결과 이벤트 추가
sealed class ActionResult {
    object Success : ActionResult()
    data class Error(val message: String) : ActionResult()
}

private val _actionResult = MutableSharedFlow<ActionResult>()
val actionResult: SharedFlow<ActionResult> = _actionResult

// accept 함수 수정
fun accept(applicationId: Long) {
    viewModelScope.launch {
        studyRepository.entryAcceptance(applicationId = applicationId, decision = "APPROVE")
            .onSuccess {
                load(currentStudyId)
                _actionResult.emit(ActionResult.Success)
            }
            .onFailure { e ->
                Log.e("RecruitingStudyApplicationViewModel", "acceptError", e)
                _actionResult.emit(ActionResult.Error(e.message ?: "수락 처리에 실패했습니다"))
            }
    }
}
🤖 Prompt for AI Agents
In
`@feature/mypage/src/main/java/com/umcspot/spot/mypage/recruiting/application/RecruitingStudyApplicationViewModel.kt`
around lines 49 - 71, Add a user-feedback result event and emit success/error
from both accept and reject: introduce a sealed ActionResult (Success /
Error(message)), back it with a private MutableSharedFlow<ActionResult> named
_actionResult and expose actionResult: SharedFlow<ActionResult>; then in
RecruitingStudyApplicationViewModel functions accept(applicationId: Long) and
reject(applicationId: Long) call studyRepository.entryAcceptance(...).onSuccess
{ load(currentStudyId); _actionResult.emit(ActionResult.Success) } and
.onFailure { Log.e(...); _actionResult.emit(ActionResult.Error(e.message ?: "처리에
실패했습니다")) } so the UI can show a toast/snackbar from actionResult.

@starshape7 starshape7 merged commit e2e447b into develop Jan 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant