Skip to content

[feat/#18] 스터디 등록하기 화면 api 연동#21

Merged
fredleeJH merged 24 commits intodevelopfrom
feat/#18-study-register
Jan 4, 2026
Merged

[feat/#18] 스터디 등록하기 화면 api 연동#21
fredleeJH merged 24 commits intodevelopfrom
feat/#18-study-register

Conversation

@fredleeJH
Copy link
Contributor

@fredleeJH fredleeJH commented Jan 3, 2026

Related issue 🛠

Work Description 📝

  • 스터디 등록하기 화면 api 연동 및 디테일 화면 세팅

Screenshot 📸

Uncompleted Tasks 😅

  • Task1

PR Point 📌

트러블 슈팅 💥

Summary by CodeRabbit

  • New Features

    • 스터디 생성 확장 — 이름, 인원, 비용, 설명, 카테고리, 스타일, 지역 코드 입력 및 서버 전송 지원
    • 이미지 첨부 기능 — 사진 선택 및 업로드 지원, 생성 시 이미지 포함 가능
    • 생성 결과 다이얼로그 — 완료 확인 모달에서 상세로 이동 가능
    • 스터디 상세 화면 추가 — 생성된 스터디의 상세 확인 가능
  • UI/네비게이션

    • 등록↔상세 네비게이션 흐름 개선
    • 성향(스타일) 선택 UI 개선 — 이진 선택 방식 도입
    • 체크 아이콘 리소스 추가 (다이얼로그 등 사용)

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

@fredleeJH fredleeJH requested a review from starshape7 January 3, 2026 09:30
@fredleeJH fredleeJH self-assigned this Jan 3, 2026
@fredleeJH fredleeJH added 🐶 종훈 🍒 [FEAT] 새로운 기능 구현 labels Jan 3, 2026
@fredleeJH fredleeJH linked an issue Jan 3, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Jan 3, 2026

Warning

Rate limit exceeded

@fredleeJH has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 6 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 00563f7 and b4ed2c5.

📒 Files selected for processing (8)
  • core/model/src/main/java/com/umcspot/spot/model/Global.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/detail/navigation/StudyDetailNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt
  • gradle/libs.versions.toml

Walkthrough

스터디 생성 기능과 관련된 멀티파트 업로드 흐름(선택 이미지 포함), DTO/모델/매퍼/리포지토리/서비스 추가 및 네비게이션·UI 변경이 도입되며, Gson 의존성과 파일 유틸, 성공 모달 컴포저블이 함께 추가됩니다.

Changes

코호트 / 파일(s) 변경 사항
빌드 / 버전 카탈로그
build-logic/convention/src/main/java/AndroidLibraryPlugin.kt, gradle/libs.versions.toml, data/study/build.gradle.kts
Gson(2.10.1) 라이브러리 추가 및 버전 카탈로그 등록; AndroidLibraryPlugingson 구현 의존 추가; data/studyimplementation(projects.core.ui) 추가; androidx-runtime-android 항목 교체
공용 유틸 / Gson 확장
core/common/src/main/java/.../FileUtil.kt, core/ui/src/main/java/.../GsonExt.kt, core/model/src/main/java/.../Global.kt
콘텐츠 URI → 임시 JPEG 파일 생성 유틸 FileUtil.createTempFileFromUri 추가; Any.toRequestBody()Any.toMultipartBodyPart(name) Gson 기반 확장 추가; StudyStyle enum 추가
디자인 시스템 / 리소스
core/designsystem/src/main/java/.../SpotActivationButton.kt, core/designsystem/src/main/res/drawable/ic_check.xml
SpotActivationButtonstyle: TextStyle 파라미터 추가(기본 h5); 체크 아이콘 벡터 리소스 추가
데이터 계층 — DTO / 매퍼
data/study/src/main/java/.../StudyRequestDto.kt, .../CreateStudyResponseDto.kt, .../StudyMapper.kt
StudyRequestDto 필드 대폭 확장(이름, 인원수, 비용정보, 설명, 카테고리, 스타일, 지역코드 등); CreateStudyResponseDto 추가; 매핑 함수들(도메인↔DTO) 업데이트
데이터 계층 — 서비스/데이터소스/리포지토리
data/study/src/main/java/.../StudyService.kt, .../StudyDataSource.kt, .../StudyDataSourceImpl.kt, .../StudyRepositoryImpl.kt
@Multipart /api/studies 엔드포인트 추가(요청: MultipartBody.Part, 이미지 optional); createStudy(...) 메서드 추가 및 multipart 구성/전달 구현; 리포지토리가 StudyDataSource로 의존 전환 및 createStudy 노출
도메인 계층
domain/study/src/main/java/.../StudyCreateModel.kt, .../StudyRepository.kt
StudyCreateModel 데이터 클래스 및 StudyPersonality enum 추가; StudyRepositorycreateStudy(studyCreateModel, imageFile) 추상 메서드 추가
UI — 스터디 등록 관련
feature/study/src/main/java/.../register/*, feature/study/src/main/java/.../component/SpotStudyDialog.kt, feature/study/src/main/java/.../register/screen/StudyIntroduceScreen.kt, .../StudyInfoScreen.kt
사진 선택 인자/콜백(selectedImageUri, onImageSelected) 추가 및 PickVisualMedia 기반 사진 피커 도입; 개인성 상태 구조를 Map<StudyPersonality, StudyStyle>로 전환; 성공 모달 SpotStudyDialog 추가; ViewModel에 이미지 처리(FileUtil) 및 리포지토리 제출 흐름 추가
UI — 스터디 상세 및 네비게이션 / 메인 네비게이션
feature/study/src/main/java/.../detail/*, feature/main/src/main/java/.../MainNavHost.kt, .../MainNavigator.kt, .../MainScreen.kt
스터디 상세 화면 및 라우트(StudyDetail) 추가; NavController 확장 navigateToStudyDetailstudyDetailGraph 추가; registerStudy 네비게이션 콜백이 navigateToStudyDetail로 변경 및 관련 네비게이션 보완
상태/뷰모델
feature/study/src/main/java/.../RegisterStudyViewModel.kt, .../RegisterStudyState.kt
personalitySelections: Map<StudyPersonality, StudyStyle> 도입, isSuccessModalVisible·createdStudyId 추가, onImageSelected(uri: String?) 추가, onPersonalityChange 시그니처 변경, 생성(submit) 흐름에서 FileUtil + studyRepository 호출로 변경

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자(UI)
    participant VM as RegisterStudyViewModel
    participant FileUtil as FileUtil
    participant Repo as StudyRepository
    participant DS as StudyDataSourceImpl
    participant Service as StudyService
    participant API as 백엔드

    User->>VM: 제출(onSubmit)
    VM->>VM: 입력 검증 및 StudyCreateModel 생성
    VM->>FileUtil: createTempFileFromUri(selectedImageUri)
    FileUtil-->>VM: File? (임시 이미지)

    VM->>Repo: createStudy(createModel, imageFile)
    Repo->>DS: createStudy(requestDtoPart, imagePart?)
    DS->>Service: POST /api/studies (multipart)
    Service->>API: HTTP 요청
    API-->>Service: BaseResponse<CreateStudyResponseDto>
    Service-->>DS: 응답 전달
    DS-->>Repo: 응답 전달
    Repo-->>VM: Result<Long> (createdStudyId)
    VM->>User: 성공 모달 표시 -> 확인 시 navigateToStudyDetail(createdStudyId)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 분

Possibly related PRs

Suggested reviewers

  • starshape7

Poem

🐰
멀티파트로 살짝 숨겨 보낸 사진 한 장,
캐시에 눌러 담아 길을 내고,
Gson으로 포장해 서버에 전송했지,
응답 받은 ID 들고 상세로 달려가요!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive 대부분의 변경사항이 StudyRegister API 연동과 관련이 있으나, 일부 UI/컴포넌트 변경(SpotActivationButton 스타일 파라미터, 새로운 아이콘)과 의존성 추가(gson, androidx-runtime)가 포함되어 있습니다. SpotActivationButton 스타일 변경과 아이콘 추가의 명확한 연관성, 그리고 이들이 StudyRegister API 연동에 필수적인지 확인이 필요합니다.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경 내용의 주요 목적과 일치합니다. 스터디 등록 화면 API 연동이라는 명확한 설명을 제공합니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으나 PR Point와 트러블 슈팅 섹션이 비어있습니다.
Linked Issues check ✅ Passed 변경 사항들이 Issue #18의 'StudyRegister API 연동' 목표를 충족합니다. API 엔드포인트, DTO, 매퍼, 리포지토리, ViewModel 등이 모두 구현되었습니다.

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: 13

🧹 Nitpick comments (11)
build-logic/convention/src/main/java/AndroidLibraryPlugin.kt (1)

26-26: Gson을 최신 버전으로 업데이트하는 것을 권장합니다.

현재 사용 중인 Gson 2.10.1은 알려진 보안 취약점이 없습니다 (CVE-2022-25647은 2.8.9에서 수정됨). 다만, 최신 버전은 2.13.1/2.13.2(2025년 9월 릴리스)이므로 최신 버그 수정, 기능 개선, 유지보수 지원을 받기 위해 업데이트를 고려하세요.

core/model/src/main/java/com/umcspot/spot/model/Global.kt (1)

62-73: StudyStyle enum에 한글 레이블 추가를 권장합니다.

StudyStyle enum이 추가되었으나, 동일 파일의 다른 enum들(ActivityType, FeeRange, StudyTheme)과 달리 한글 레이블이 없습니다. UI에 표시될 가능성을 고려하면 일관성을 위해 label 프로퍼티를 추가하는 것이 좋습니다.

🔎 한글 레이블 추가 예시
 enum class StudyStyle(
+    val label: String
+) {
-    NETWORKING,
-    GOAL_OR_RULE_ORIENTED,
-    SHORT_TERM,
-    LONG_TERM,
-    INDIVIDUAL_PLUS_DISCUSSION,
-    GROUP_PLUS_SIMULTANEOUS,
-    LEARNING_BASED,
-    DISCUSSION_BASED,
-    LIGHT_AND_FLEXIBLE,
-    STRUCTURED_AND_PLANNED
+    NETWORKING("네트워킹"),
+    GOAL_OR_RULE_ORIENTED("목표/규칙 지향"),
+    SHORT_TERM("단기"),
+    LONG_TERM("장기"),
+    INDIVIDUAL_PLUS_DISCUSSION("개인+토론"),
+    GROUP_PLUS_SIMULTANEOUS("그룹+동시"),
+    LEARNING_BASED("학습 중심"),
+    DISCUSSION_BASED("토론 중심"),
+    LIGHT_AND_FLEXIBLE("가볍고 유연"),
+    STRUCTURED_AND_PLANNED("체계적이고 계획적")
 }
feature/study/src/main/java/com/umcspot/spot/study/component/SpotStudyDialog.kt (2)

23-23: 사용되지 않는 import 제거

B500은 직접 사용되지 않고 SpotTheme.colors.B500을 통해 접근하고 있습니다.

🔎 제안된 수정
-import com.umcspot.spot.designsystem.theme.B500

68-78: 닫기 버튼 터치 영역 확인

IconButton 크기가 20.dp로 설정되어 있습니다. Material3의 IconButton은 기본적으로 최소 터치 영역을 보장하지만, 명시적으로 작은 크기를 지정하면 접근성에 영향을 줄 수 있습니다. 터치 영역이 충분한지 확인해 주세요.

data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt (1)

39-40: 함수 사이에 빈 줄 추가

toDomainList()CreateStudyResponseDto.toDomain() 함수 사이에 빈 줄이 없어 가독성이 떨어집니다.

🔎 제안된 수정
 fun StudyResponseDto.toDomainList(): StudyResultList =
     StudyResultList(
         studyList = this.studyList.map(DTOStudy::toDomain)
     )
+
 fun CreateStudyResponseDto.toDomain(): Long = this.studyId
feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt (1)

174-176: Import된 extension 함수 사용 권장

navigateToStudyDetail extension 함수가 import되어 있지만 (line 32), 직접 navController.navigate(StudyDetail(studyId), navOptions)를 호출하고 있습니다. 일관성을 위해 extension 함수를 사용하는 것이 좋습니다.

🔎 제안된 수정
     fun navigateToStudyDetail(studyId: Long, navOptions: NavOptions? = null) {
-        navController.navigate(StudyDetail(studyId), navOptions)
+        navController.navigateToStudyDetail(studyId, navOptions)
     }
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt (2)

102-104: 불필요한 recoverCatching 제거

recoverCatching { throw exception }은 예외를 다시 던지기만 하므로 의미가 없습니다. 복구 로직이 없다면 제거하는 것이 좋습니다.

🔎 제안된 수정
-    }.recoverCatching { exception ->
-        throw exception
-    }
+    }

83-95: DataSource 레이어 우회

createStudystudyService를 직접 호출하고 있어 StudyDataSourceImpl에 구현된 동일한 로직과 중복됩니다. 다른 메서드들과의 일관성을 위해 DataSource를 통해 호출하거나, DataSource의 해당 메서드를 제거하는 것이 좋습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (1)

206-212: onIntroduceValid 콜백이 no-op입니다.

onIntroduceValid = { }로 전달되어 StudyIntroduceScreen에서 유효성 검사 결과가 무시됩니다. 이 콜백이 필요하지 않다면 StudyIntroduceScreen에서 제거하거나, 필요하다면 실제 로직을 구현해야 합니다.

domain/study/src/main/java/com/umcspot/spot/study/model/StudyCreateModel.kt (2)

49-51: getStyle 함수가 유효하지 않은 인덱스를 조용히 처리합니다.

index가 0이 아닌 모든 값에 대해 option2를 반환합니다. 유효한 인덱스가 0과 1뿐이라면, 잘못된 인덱스에 대해 명시적으로 처리하는 것이 더 안전합니다.

🔎 제안된 수정 사항
     fun getStyle(index: Int): StudyStyle {
-        return if (index == 0) option1 else option2
+        return when (index) {
+            0 -> option1
+            1 -> option2
+            else -> throw IllegalArgumentException("Invalid style index: $index")
+        }
     }

또는 nullable로 변경:

fun getStyle(index: Int): StudyStyle? = when (index) {
    0 -> option1
    1 -> option2
    else -> null
}

53-55: entriesList는 불필요할 수 있습니다.

Kotlin enum의 entries는 이미 EnumEntries<StudyPersonality> 타입으로 제공되며, List처럼 사용할 수 있습니다. toList()로 복사본을 만들 특별한 이유가 없다면 직접 entries를 사용해도 됩니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56f4e71 and 0966055.

📒 Files selected for processing (29)
  • build-logic/convention/src/main/java/AndroidLibraryPlugin.kt
  • core/common/src/main/java/com/umcspot/spot/common/util/FileUtil.kt
  • core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SpotActivationButton.kt
  • core/designsystem/src/main/res/drawable/ic_check.xml
  • core/model/src/main/java/com/umcspot/spot/model/Global.kt
  • core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt
  • data/study/build.gradle.kts
  • data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt
  • data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt
  • data/study/src/main/java/com/umcspot/spot/study/dto/request/StudyRequestDto.kt
  • data/study/src/main/java/com/umcspot/spot/study/dto/response/CreateStudyResponseDto.kt
  • data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt
  • data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
  • data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt
  • domain/study/src/main/java/com/umcspot/spot/study/model/StudyCreateModel.kt
  • domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt
  • feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/component/SpotStudyDialog.kt
  • feature/study/src/main/java/com/umcspot/spot/study/detail/StudyDetailScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/detail/navigation/StudyDetailNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/navigation/RegisterStudyNavigation.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt
  • feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt
  • gradle/libs.versions.toml
🧰 Additional context used
🧬 Code graph analysis (6)
feature/study/src/main/java/com/umcspot/spot/study/register/navigation/RegisterStudyNavigation.kt (1)
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (1)
  • RegisterStudyRoute (41-128)
feature/study/src/main/java/com/umcspot/spot/study/component/SpotStudyDialog.kt (2)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (2)
  • screenHeightDp (11-16)
  • screenWidthDp (18-23)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SpotActivationButton.kt (1)
  • SpotActivationButton (25-59)
feature/study/src/main/java/com/umcspot/spot/study/detail/StudyDetailScreen.kt (2)
core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/appBar/AppBar.kt (1)
  • BackTopBar (113-143)
core/ui/src/main/java/com/umcspot/spot/ui/extension/DimensionExt.kt (1)
  • screenWidthDp (18-23)
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt (3)
feature/study/src/main/java/com/umcspot/spot/study/component/SpotStudyDialog.kt (1)
  • SpotStudyDialog (28-47)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt (1)
  • StudyInfoScreen (22-106)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt (1)
  • StudyIntroduceScreen (46-167)
feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt (2)
feature/study/src/main/java/com/umcspot/spot/study/register/component/BinaryChoiceRow.kt (1)
  • BinaryChoiceRow (17-55)
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)
  • onPersonalityChange (142-151)
feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt (1)
feature/study/src/main/java/com/umcspot/spot/study/detail/navigation/StudyDetailNavigation.kt (1)
  • studyDetailGraph (17-30)
🔇 Additional comments (26)
data/study/build.gradle.kts (1)

10-10: core.ui 의존성이 실제로 사용되지 않고 있습니다.

data/study 모듈의 모든 Kotlin 파일을 검사한 결과, core.ui에서 어떤 요소도 import되지 않고 있습니다. GsonExt 유틸리티도 사용되지 않으며, DTO들은 @Serializable (kotlinx.serialization)을 사용하고 있습니다. 사용되지 않는 이 의존성은 제거하는 것이 좋습니다.

Likely an incorrect or invalid review comment.

feature/study/src/main/java/com/umcspot/spot/study/register/model/RegisterStudyState.kt (4)

5-7: LGTM!

새로운 personalitySelections 맵을 지원하기 위한 임포트가 적절하게 추가되었습니다.


23-23: 개별 필드를 맵으로 통합한 리팩토링이 좋습니다.

여러 개별 선호도 필드를 Map<StudyPersonality, StudyStyle>로 통합하여 유지보수성과 확장성이 향상되었습니다. 맵 구조는 자연스럽게 중복 키를 방지합니다.


28-29: 추가 검증이 필요하지 않습니다. 상태 불변성이 올바르게 관리되고 있습니다.

검증 결과, isSuccessModalVisiblecreatedStudyId는 RegisterStudyViewModel의 성공 콜백(lines 217-218)에서 항상 함께 업데이트되므로 불변성이 적절하게 유지됩니다. 모달이 표시될 때 createdStudyId는 항상 유효한 값으로 설정되며, 두 필드가 분리되어 업데이트되는 경로는 없습니다.


32-34: NavigateToHome 제거가 올바르게 마이그레이션되었습니다.

RegisterStudySideEffect에서 NavigateToHome 제거는 완전하게 구현되었습니다. 네비게이션이 사이드 이펙트 기반에서 상태 기반으로 변경되었으며, RegisterStudyScreen에서 createdStudyId?.let(navigateToStudyDetail)을 통해 일관되게 처리되고 있습니다. 마이그레이션이 완료되었으므로 추가 조치는 불필요합니다.

core/designsystem/src/main/java/com/umcspot/spot/designsystem/component/button/SpotActivationButton.kt (1)

16-16: LGTM! 좋은 개선입니다.

텍스트 스타일을 파라미터화하여 컴포넌트의 재사용성이 향상되었습니다. 기본값을 제공하여 기존 코드와의 호환성도 유지됩니다.

Also applies to: 31-31, 55-55

feature/study/src/main/java/com/umcspot/spot/study/detail/StudyDetailScreen.kt (1)

23-79: 스터디 상세 화면 스켈레톤 구조가 적절합니다.

화면의 기본 구조와 네비게이션 처리가 올바르게 구현되었습니다. BackHandler를 통한 뒤로가기 처리도 적절합니다. 향후 실제 스터디 데이터를 표시하는 로직이 추가될 예정인 것으로 보입니다.

core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt (1)

14-17: 멀티파트 바디 파트 생성 로직이 적절합니다.

JSON 데이터를 multipart form data로 변환하는 구현이 올바릅니다. filenamenull로 전달하는 것은 JSON 데이터 파트에 적합합니다.

gradle/libs.versions.toml (2)

104-105: Gson 라이브러리 선언이 올바르게 추가되었습니다.

버전 카탈로그에 Gson이 적절히 선언되었습니다. 버전 확인은 AndroidLibraryPlugin.kt 리뷰에서 요청한 검증 스크립트를 통해 진행해주세요.

Also applies to: 279-280


267-267: androidx-runtime 라이브러리 정의 재검토 필요.

androidx-runtime-android 변경에 대한 초기 우려는 타당하지 않습니다. 코드베이스에서 androidx-runtime-android는 존재하지 않으며, 현재 androidx-runtime 별칭도 실제로 어떤 빌드 파일에서도 사용되지 않습니다. 모든 Compose runtime 의존성은 소스 코드에서 직접 가져오기로 처리되어 있습니다. 이 라이브러리 별칭이 필요한지 재검토하는 것을 권장합니다.

Likely an incorrect or invalid review comment.

feature/study/src/main/java/com/umcspot/spot/study/detail/navigation/StudyDetailNavigation.kt (2)

17-30: 잘 구현되었습니다.

네비게이션 그래프 설정이 올바르게 구현되었으며, backStackEntry에서 studyId를 적절히 추출하여 StudyDetailRoute에 전달하고 있습니다.


32-33: 직렬화 가능한 라우트 정의가 적절합니다.

@Serializable 어노테이션과 Route 인터페이스 구현이 올바르게 설정되어 있으며, 네비게이션 인자 전달을 위한 구조가 적절합니다.

core/designsystem/src/main/res/drawable/ic_check.xml (1)

1-9: 체크 아이콘 리소스가 올바르게 정의되었습니다.

벡터 드로어블 구문이 올바르며, 체크 마크 아이콘이 적절하게 구현되었습니다.

domain/study/src/main/java/com/umcspot/spot/study/repository/StudyRepository.kt (1)

28-28: 스터디 생성 메서드가 적절하게 정의되었습니다.

도메인 모델(StudyCreateModel)을 사용하고, 선택적 이미지 파일을 지원하며, Result 래퍼를 통한 에러 핸들링이 올바르게 구현되었습니다. 생성된 스터디 ID를 Long 타입으로 반환하는 것도 적절합니다.

data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt (1)

22-22: 데이터 소스 메서드가 올바르게 정의되었습니다.

데이터 레이어에 적합하게 DTO(StudyRequestDto, CreateStudyResponseDto)를 사용하고 있으며, BaseResponse 래퍼를 통한 일관된 응답 처리가 이루어지고 있습니다.

core/model/src/main/java/com/umcspot/spot/model/Global.kt (1)

9-9: 코드 스타일 변경 확인됨.

기존 enum들에 대한 포맷팅 변경(공백, 줄바꿈 등)이 적용되었습니다. 이는 IDE 자동 포맷팅으로 보이며 동작에는 영향이 없습니다.

Also applies to: 20-23, 28-30, 35-37

feature/main/src/main/java/com/umcspot/spot/main/MainScreen.kt (1)

50-54: RegisterStudy와 StudyDetail 화면에 대한 TopBar 처리가 적절합니다.

두 화면에 대해 통합된 조건(isRegisterOrDetail)으로 TopBar를 숨기는 로직이 올바르게 구현되었습니다.

feature/study/src/main/java/com/umcspot/spot/study/register/navigation/RegisterStudyNavigation.kt (1)

21-21: 스터디 생성 후 상세 화면으로 이동하는 네비게이션이 올바르게 구현되었습니다.

navigateToHome에서 navigateToStudyDetail(Long)으로 변경하여 생성된 스터디의 ID를 받아 상세 화면으로 이동하는 흐름이 적절합니다.

Also applies to: 28-28

feature/study/src/main/java/com/umcspot/spot/study/component/SpotStudyDialog.kt (1)

107-115: LGTM!

SpotActivationButton 사용이 적절하며, 테마 typography와 spacing이 일관되게 적용되었습니다.

data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt (1)

11-22: LGTM!

StudyCreateModel에서 StudyRequestDto로의 매핑이 명확하고 모든 필드가 올바르게 전달됩니다.

feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyIntroduceScreen.kt (1)

57-62: LGTM!

Photo Picker와 AsyncImage를 사용한 이미지 선택 플로우가 올바르게 구현되었습니다. PickVisualMedia contract 사용과 URI 처리가 적절합니다.

Also applies to: 150-156

feature/main/src/main/java/com/umcspot/spot/main/MainNavigator.kt (1)

142-144: LGTM!

navigateToMyStudy 함수가 올바르게 위임되어 있습니다.

feature/main/src/main/java/com/umcspot/spot/main/MainNavHost.kt (1)

111-127: LGTM!

스터디 등록 후 상세 화면으로의 네비게이션 플로우가 올바르게 구성되었습니다. popUpTo<RegisterStudy> { inclusive = true }로 등록 화면을 스택에서 제거하고 상세 화면으로 이동하는 것이 적절합니다.

feature/study/src/main/java/com/umcspot/spot/study/register/screen/StudyInfoScreen.kt (1)

82-102: LGTM!

StudyPersonality.entries를 순회하면서 동적으로 레이블과 선택 상태를 매핑하는 방식이 이전의 인덱스 기반 접근보다 타입 안전하고 유지보수하기 좋습니다. selectedIndex 계산 로직도 정확합니다.

data/study/src/main/java/com/umcspot/spot/study/dto/request/StudyRequestDto.kt (1)

23-27: Enum 값의 Gson 직렬화 설정이 누락되었습니다.

StudyThemeStudyStyle enum을 직접 사용하지만 커스텀 직렬화가 설정되지 않았습니다. Gson은 기본적으로 enum의 name() 메서드를 사용하므로:

  • StudyTheme"LANGUAGE", "CERTIFICATION" 등으로 직렬화되는데, enum이 "어학", "자격증" 같은 한글 제목을 가지고 있어 API 예상 형식과 맞지 않을 수 있습니다.
  • StudyStyle"NETWORKING", "GOAL_OR_RULE_ORIENTED" 등으로 직렬화되는데 서버 API 명세와 일치하는지 확인이 필요합니다.

각 enum 값에 @SerializedName 어노테이션을 추가하거나, 커스텀 TypeAdapter를 구현하여 적절한 직렬화 형식을 정의해주세요.

feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt (1)

195-197: 지역 코드 매핑 실패가 무시됩니다.

selectedRegions의 값은 검색 결과의 LocationRow.name에서만 선택되고, allLocations은 같은 소스(LocationStore)에서 캐시되므로 실제 불일치 가능성은 낮습니다. 다만 mapNotNull을 사용하여 찾지 못한 지역이 조용히 필터링되므로, 지역 코드 매핑이 누락되어도 사용자에게 알려지지 않습니다. 선택한 지역이 코드로 변환되지 않은 경우를 감지하고 처리하는 로직(예: 로깅, 유효성 검사 또는 예외 처리)이 필요합니다.

Comment on lines +12 to +32
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
val inputStream = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()

if (bitmap == null) return null

val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)

FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}

bitmap.recycle()

file
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

리소스 누수 위험: InputStream이 예외 발생 시 닫히지 않을 수 있습니다.

Line 14에서 열린 inputStream이 Line 15의 decodeStream에서 예외가 발생하면 Line 16의 close()가 실행되지 않아 리소스 누수가 발생합니다. use() 블록을 사용하여 예외 발생 여부와 관계없이 스트림이 닫히도록 보장해야 합니다.

🔎 리소스 관리 개선 제안
 fun createTempFileFromUri(context: Context, uri: Uri): File? {
     return runCatching {
-        val inputStream = context.contentResolver.openInputStream(uri)
-        val bitmap = BitmapFactory.decodeStream(inputStream)
-        inputStream?.close()
-
-        if (bitmap == null) return null
+        context.contentResolver.openInputStream(uri)?.use { inputStream ->
+            val bitmap = BitmapFactory.decodeStream(inputStream)
+                ?: return null
 
-        val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
+            val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
 
-        FileOutputStream(file).use { outputStream ->
-            bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
-        }
+            FileOutputStream(file).use { outputStream ->
+                bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+            }
 
-        bitmap.recycle()
+            bitmap.recycle()
 
-        file
+            file
+        }
     }.onFailure { e ->
         Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
     }.getOrNull()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
val inputStream = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()
if (bitmap == null) return null
val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}
bitmap.recycle()
file
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
fun createTempFileFromUri(context: Context, uri: Uri): File? {
return runCatching {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val bitmap = BitmapFactory.decodeStream(inputStream)
?: return null
val file = File.createTempFile("upload_image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
}
bitmap.recycle()
file
}
}.onFailure { e ->
Timber.e(e, "이미지 압축 및 임시 파일 생성 실패")
}.getOrNull()
}
🤖 Prompt for AI Agents
In core/common/src/main/java/com/umcspot/spot/common/util/FileUtil.kt around
lines 12 to 32, the InputStream opened from contentResolver may not be closed if
BitmapFactory.decodeStream throws, causing a resource leak; wrap the InputStream
in a Kotlin use{} block (or apply try-with-resources equivalent) so it is closed
regardless of exceptions, move decodeStream and any downstream work that depends
on the stream inside that use block, handle a null bitmap after the use block
(or return early inside it) and keep the existing runCatching/onFailure behavior
for logging.

Comment on lines +9 to +12
fun Any.toRequestBody(): RequestBody {
val jsonString = Gson().toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
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

Gson 인스턴스를 재사용하여 성능을 개선하세요.

매번 새로운 Gson() 인스턴스를 생성하면 불필요한 오버헤드가 발생합니다. Gson 인스턴스는 스레드 안전하므로 싱글톤으로 공유해야 합니다.

🔎 제안하는 수정사항
+private val gson = Gson()
+
 fun Any.toRequestBody(): RequestBody {
-    val jsonString = Gson().toJson(this)
+    val jsonString = gson.toJson(this)
     return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun Any.toRequestBody(): RequestBody {
val jsonString = Gson().toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
private val gson = Gson()
fun Any.toRequestBody(): RequestBody {
val jsonString = gson.toJson(this)
return jsonString.toRequestBody("application/json".toMediaTypeOrNull())
}
🤖 Prompt for AI Agents
In core/ui/src/main/java/com/umcspot/spot/ui/extension/GsonExt.kt around lines 9
to 12, the extension creates a new Gson() on every call which adds unnecessary
overhead; replace per-call instantiation with a shared singleton Gson instance
(declare a private top-level immutable Gson and use it in toRequestBody) so the
extension reuses the thread-safe Gson across calls.

Comment on lines +6 to +10
@Serializable
data class CreateStudyResponseDto(
@SerialName("studyId")
val studyId: Long
) No newline at end of file
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 프로젝트에서 kotlinx.serialization과 Gson 사용 패턴 확인

echo "=== kotlinx.serialization 사용 현황 ==="
rg -n --type=kotlin "@Serializable|@SerialName" | head -20

echo -e "\n=== Gson 사용 현황 ==="
rg -n --type=kotlin "@SerializedName|import com.google.gson" | head -20

echo -e "\n=== Gradle 의존성 확인 ==="
fd -e gradle -e kts -x grep -l "serialization\|gson" {}

echo -e "\n=== 전체 DTO 파일 현황 ==="
fd "Dto.kt$" | head -15

Repository: SPOTeam/Android-V2

Length of output: 4749


🏁 Script executed:

#!/bin/bash
# Gradle 의존성 파일 내용 확인
fd -e gradle -e kts | xargs grep -l "serialization\|gson" | head -5 | xargs cat

Repository: SPOTeam/Android-V2

Length of output: 1827


🏁 Script executed:

cat data/study/build.gradle.kts

Repository: SPOTeam/Android-V2

Length of output: 257


🏁 Script executed:

# 다른 DTO들의 직렬화 어노테이션 사용 패턴 확인
rg -n "@Serializable|@SerializedName" data/ -A 2 -B 2 | head -50

Repository: SPOTeam/Android-V2

Length of output: 4482


CreateStudyResponseDto는 올바른 직렬화 라이브러리를 사용합니다. 대신 같은 모듈의 StudyRequestDto와 일관성을 맞추세요.

이 파일은 프로젝트 전역 표준인 kotlinx.serialization을 올바르게 사용하고 있습니다. 그러나 같은 data/study 모듈에 속한 StudyRequestDto.kt는 Gson의 @SerializedName을 사용하고 있어 모듈 내 불일치가 발생합니다. 프로젝트는 네비게이션, 코어/네트워크, 다른 데이터 모듈 전반에서 kotlinx.serialization을 일관되게 사용하므로, StudyRequestDto.kt도 같은 라이브러리로 통일해야 합니다.

🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/dto/response/CreateStudyResponseDto.kt
around lines 6-10: the module mixes kotlinx.serialization (here) with Gson
annotations used in StudyRequestDto.kt, causing inconsistency; update
StudyRequestDto.kt to use kotlinx.serialization to match the module/project
standard by annotating the class with @Serializable, replacing any Gson
@SerializedName usages with @SerialName, importing kotlinx.serialization
annotations, and ensuring the module has the kotlinx.serialization
plugin/dependency configured; adjust any serialization-specific types or default
values to be compatible with kotlinx.serialization and run a build to verify no
remaining Gson references.

Comment on lines 97 to 101
if (!response.isSuccess) {
throw Exception(response.message ?: "스터디 생성 실패")
}

response.result.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

response.result null 체크 추가 권장

response.isSuccess가 true여도 response.result가 null일 수 있습니다. null-safe 접근자 사용을 권장합니다.

🔎 제안된 수정
         if (!response.isSuccess) {
             throw Exception(response.message ?: "스터디 생성 실패")
         }

-        response.result.studyId
+        response.result?.studyId ?: throw Exception("스터디 ID를 받지 못했습니다")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response.isSuccess) {
throw Exception(response.message ?: "스터디 생성 실패")
}
response.result.studyId
if (!response.isSuccess) {
throw Exception(response.message ?: "스터디 생성 실패")
}
response.result?.studyId ?: throw Exception("스터디 ID를 받지 못했습니다")
🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 97 to 101, add a null check for response.result before accessing
studyId: if response.result is null, throw an Exception (or a more specific
domain exception) with an appropriate message; use a null-safe access pattern
(e.g., elvis operator) to validate result and then return result.studyId when
non-null.

Comment on lines +65 to +77
if (uiState.isSuccessModalVisible) {
SpotStudyDialog(
onDismissRequest = {
uiState.createdStudyId?.let(navigateToStudyDetail)
},
title = "스터디 등록 완료",
description = "이제 스터디 모집이 시작됩니다!\n마이페이지에서 신청을 수락할 수 있어요.",
buttonText = "내 스터디 보러가기",
onButtonClick = {
uiState.createdStudyId?.let(navigateToStudyDetail)
}
)
}
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

다이얼로그가 닫히지 않는 문제가 있습니다.

onDismissRequestonButtonClick 모두 navigateToStudyDetail만 호출하고 isSuccessModalVisiblefalse로 업데이트하지 않습니다. 다이얼로그가 닫히기 전에 네비게이션이 발생하면 문제가 없을 수 있지만, 네비게이션 실패 시 다이얼로그가 계속 표시될 수 있습니다.

또한 createdStudyIdnull인 경우 아무 동작도 하지 않아 사용자가 다이얼로그에서 벗어날 수 없습니다.

🔎 제안된 수정 사항
     if (uiState.isSuccessModalVisible) {
         SpotStudyDialog(
             onDismissRequest = {
-                uiState.createdStudyId?.let(navigateToStudyDetail)
+                viewModel.dismissSuccessModal()
+                uiState.createdStudyId?.let(navigateToStudyDetail) ?: onBackClick()
             },
             title = "스터디 등록 완료",
             description = "이제 스터디 모집이 시작됩니다!\n마이페이지에서 신청을 수락할 수 있어요.",
             buttonText = "내 스터디 보러가기",
             onButtonClick = {
-                uiState.createdStudyId?.let(navigateToStudyDetail)
+                viewModel.dismissSuccessModal()
+                uiState.createdStudyId?.let(navigateToStudyDetail) ?: onBackClick()
             }
         )
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 82 to 84
is RegisterStudySideEffect.ShowSnackBar -> {
}
}
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

ShowSnackBar 사이드 이펙트 핸들러가 비어 있습니다.

API 호출 실패 시 ShowSnackBar 사이드 이펙트가 발생하지만 실제로 스낵바가 표시되지 않습니다. 사용자에게 오류 피드백이 전달되지 않습니다.

🔎 제안된 수정 사항

스낵바 호스트를 추가하거나, 사이드 이펙트를 제거하고 다른 방식으로 오류를 표시해야 합니다:

+val snackbarHostState = remember { SnackbarHostState() }
+
 LaunchedEffect(viewModel.sideEffect) {
     viewModel.sideEffect.collectLatest { effect ->
         when (effect) {
             is RegisterStudySideEffect.ShowSnackBar -> {
+                snackbarHostState.showSnackbar(effect.message)
             }
         }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyScreen.kt
around lines 82-84, the ShowSnackBar side-effect handler is empty so API
failures produce no visible feedback; update the handler to display a Snackbar
by invoking scaffoldState.snackbarHostState.showSnackbar (or using a remembered
SnackbarHostState) inside a coroutine: obtain a CoroutineScope (e.g.,
rememberCoroutineScope or use LaunchedEffect with collected side-effects), call
showSnackbar with the side-effect message when ShowSnackBar is received, and
ensure a SnackbarHost/Scaffold with the same snackbarHostState is present in the
composable so the message is visible to the user.

Comment on lines 184 to 229
fun submit() {
viewModelScope.launch {
_sideEffect.emit(RegisterStudySideEffect.NavigateToHome)
val currentState = _uiState.value

val imageFile = currentState.studyImageUri?.let { uriString ->
FileUtil.createTempFileFromUri(appContext, uriString.toUri())
}

val regionCodes = if (currentState.activityType == ActivityType.ONLINE) {
emptyList()
} else {
currentState.selectedRegions.mapNotNull { regionName ->
allLocations.find { it.name == regionName }?.code
}
}

val styles = currentState.personalitySelections.values.toList()

val createModel = StudyCreateModel(
name = currentState.studyName,
maxMembers = currentState.memberCount,
hasFee = currentState.hasFee ?: false,
amount = currentState.feeAmount.toIntOrNull() ?: 0,
description = currentState.description,
categories = currentState.studyThemes,
styles = styles,
regionCodes = regionCodes
)

studyRepository.createStudy(createModel, imageFile)
.onSuccess { id ->
_uiState.update {
it.copy(
isSuccessModalVisible = true,
createdStudyId = id
)
}
}
.onFailure { exception ->
_sideEffect.emit(
RegisterStudySideEffect.ShowSnackBar(
exception.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

API 호출 중 로딩 상태가 없습니다.

submit() 함수가 비동기로 API를 호출하지만 로딩 상태를 관리하지 않습니다. 사용자가 버튼을 여러 번 누르면 중복 요청이 발생할 수 있고, 사용자에게 진행 상황에 대한 피드백이 없습니다.

🔎 제안된 수정 사항

RegisterStudyStateisLoading 필드를 추가하고 API 호출 전후로 상태를 업데이트하세요:

 fun submit() {
+    if (_uiState.value.isLoading) return
+    
     viewModelScope.launch {
+        _uiState.update { it.copy(isLoading = true) }
+        
         val currentState = _uiState.value
         // ... existing code ...
         
         studyRepository.createStudy(createModel, imageFile)
             .onSuccess { id ->
                 _uiState.update {
                     it.copy(
+                        isLoading = false,
                         isSuccessModalVisible = true,
                         createdStudyId = id
                     )
                 }
             }
             .onFailure { exception ->
+                _uiState.update { it.copy(isLoading = false) }
                 _sideEffect.emit(
                     RegisterStudySideEffect.ShowSnackBar(
                         exception.message ?: "실패"
                     )
                 )
             }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt
around lines 184 to 229, add and use a loading flag: extend RegisterStudyState
with an isLoading Boolean (default false), set isLoading = true before starting
the API call in submit(), and set isLoading = false in both success and failure
paths (including finally if using try/catch) so duplicate submissions are
prevented and UI can show progress; also ensure UI consumes isLoading to disable
the submit button and show a spinner, and emit error/success side effects as
currently implemented.

Comment on lines +188 to +190
val imageFile = currentState.studyImageUri?.let { uriString ->
FileUtil.createTempFileFromUri(appContext, uriString.toUri())
}
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

🧩 Analysis chain

🏁 Script executed:

# Search for FileUtil.createTempFileFromUri implementation
rg -A 20 "fun createTempFileFromUri" --type kotlin

Repository: SPOTeam/Android-V2

Length of output: 2170


🏁 Script executed:

# Find the FileUtil file location
fd -t f "FileUtil.kt"

Repository: SPOTeam/Android-V2

Length of output: 128


🏁 Script executed:

# Look at the RegisterStudyViewModel file to see how imageFile is used after line 190
rg -n "imageFile" feature/study/src/main/java/com/umcspot/spot/study/register/RegisterStudyViewModel.kt -A 5 -B 5

Repository: SPOTeam/Android-V2

Length of output: 954


이미지 파일 생성 실패 시 사용자 알림이 필요합니다.

FileUtil.createTempFileFromUri는 내부적으로 예외를 처리하고 Timber로 로깅하지만, 실패 시 null을 반환합니다. 현재 코드는 imageFilenull인지 검증하지 않고 바로 createStudy에 전달하므로, 이미지 선택 후 파일 생성이 실패해도 사용자에게는 아무런 피드백이 없습니다. 이미지 생성 실패 시 사용자에게 오류 메시지를 표시하거나 제출을 방지해야 합니다.

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: 5

♻️ Duplicate comments (2)
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt (2)

64-77: response.result null 체크 추가 권장 (이전 리뷰 미해결)

response.isSuccess가 true여도 response.result가 null일 수 있습니다. null-safe 접근자 사용을 권장합니다.

🔎 제안된 수정
         if (!response.isSuccess) {
             throw Exception(response.message ?: "스터디 생성 실패")
         }

-        response.result.studyId
+        response.result?.studyId ?: throw Exception("스터디 ID를 받지 못했습니다")

39-46: response.result null 체크 필수

response.result가 null인 경우 toDomain() 호출 시 NPE가 발생합니다.

🔎 제안된 수정
         val response = studyDataSource.getRecruitingStudies(
             sortType = sortType,
             activityType = activityType ?: ActivityType.OFFLINE,
             theme = theme ?: StudyTheme.OTHER,
             feeRange = feeRange ?: FeeRange.NONE
         )
-        response.result.toDomain()
+        response.result?.toDomain() ?: throw Exception("모집 중인 스터디 결과를 받지 못했습니다")
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82d077d and 00563f7.

📒 Files selected for processing (2)
  • data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt
  • data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
🔇 Additional comments (5)
data/study/src/main/java/com/umcspot/spot/study/mapper/StudyMapper.kt (4)

6-6: LGTM!

StudyCreateModel로 import를 변경한 것은 스터디 생성 목적에 맞게 명확한 네이밍을 사용하여 코드 가독성을 향상시킵니다.


21-30: LGTM!

DTO에서 도메인 모델로의 매핑 로직이 올바르며, 코드 포맷팅도 일관성 있게 개선되었습니다.


32-34: 좋은 리팩토링입니다!

메서드 이름을 toDomain()으로 변경하고 .map { it.toDomain() }을 사용한 것은 Kotlin 관용구에 맞는 깔끔한 구현입니다. 코드 가독성과 일관성이 향상되었습니다.


10-19: 이미지 업로드 처리가 올바르게 구현되어 있습니다.

매핑 로직이 정상이며, 이미지 처리도 아키텍처에 맞게 구현되어 있습니다. StudyDataSourceImpl.createStudy()에서 StudyRequestDto와 이미지 파일을 별도의 MultipartBody.Part로 변환하여 @Multipart 주석이 있는 StudyService.createStudy()로 전달합니다. 매퍼는 DTO 필드 매핑에만 집중하고, 멀티파트 파일 처리는 DataSource 계층에서 담당하는 구조로 관심사의 분리가 잘 되어 있습니다.

data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt (1)

16-18: 의존성 리팩토링 승인

StudyService에서 StudyDataSource로 변경한 것은 데이터 레이어의 책임 분리를 명확히 하는 좋은 리팩토링입니다.

Comment on lines 20 to 24
override suspend fun getPopularStudies(): Result<StudyResultList> =
runCatching {
val response = studyService.getPopularStudies()
response.result!!.toDomainList()
}.recoverCatching {
setPopularDummies()
val response = studyDataSource.getPopularStudies()
response.result.toDomain()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

response.result null 체크 필수

response.result가 null인 경우 toDomain() 호출 시 NPE가 발생합니다. null-safe 접근자 사용이 필요합니다.

🔎 제안된 수정
 override suspend fun getPopularStudies(): Result<StudyResultList> =
     runCatching {
         val response = studyDataSource.getPopularStudies()
-        response.result.toDomain()
+        response.result?.toDomain() ?: throw Exception("인기 스터디 결과를 받지 못했습니다")
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override suspend fun getPopularStudies(): Result<StudyResultList> =
runCatching {
val response = studyService.getPopularStudies()
response.result!!.toDomainList()
}.recoverCatching {
setPopularDummies()
val response = studyDataSource.getPopularStudies()
response.result.toDomain()
}
override suspend fun getPopularStudies(): Result<StudyResultList> =
runCatching {
val response = studyDataSource.getPopularStudies()
response.result?.toDomain() ?: throw Exception("인기 스터디 결과를 받지 못했습니다")
}
🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 20-24, response.result can be null and calling toDomain() causes an
NPE; update the implementation to null-check response.result before invoking
toDomain() and handle the null case (for example, return a meaningful
Result.failure with a specific exception or map to an empty StudyResultList)
using the null-safe operator or an explicit if/else so no NPE occurs.

Comment on lines 26 to 30
override suspend fun getRecommendStudies(): Result<StudyResultList> =
runCatching {
val response = studyService.getPopularStudies()
response.result!!.toDomainList()
}.recoverCatching {
setRecommendDummies()
val response = studyDataSource.getRecommendStudies()
response.result.toDomain()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

response.result null 체크 필수

response.result가 null인 경우 toDomain() 호출 시 NPE가 발생합니다.

🔎 제안된 수정
 override suspend fun getRecommendStudies(): Result<StudyResultList> =
     runCatching {
         val response = studyDataSource.getRecommendStudies()
-        response.result.toDomain()
+        response.result?.toDomain() ?: throw Exception("추천 스터디 결과를 받지 못했습니다")
     }
🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 26 to 30, response.result is used without a null check which can
cause an NPE when calling toDomain(); add a null-safe handling: check if
response.result is null and either return a sensible empty StudyResultList
(e.g., StudyResultList.empty() or with an empty list) or throw a clear
IllegalStateException inside runCatching so the failure is captured, then call
toDomain() only when result is non-null.

Comment on lines +39 to +43
val response = studyDataSource.getRecruitingStudies(
sortType = sortType,
activityType = activityType ?: ActivityType.OFFLINE,
theme = theme ?: StudyTheme.OTHER,
feeRange = feeRange ?: FeeRange.NONE
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the StudyDataSource file
fd -t f -e kt StudyDataSource | head -20

Repository: SPOTeam/Android-V2

Length of output: 225


🏁 Script executed:

#!/bin/bash
# Also search for the method signature in the codebase
rg "fun getRecruitingStudies" -A 5 -t kt

Repository: SPOTeam/Android-V2

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Look at the complete StudyRepositoryImpl.kt file to understand context
fd -t f -e kt StudyRepositoryImpl | head -5

Repository: SPOTeam/Android-V2

Length of output: 147


🏁 Script executed:

#!/bin/bash
# Read the StudyDataSource interface to understand the method signature
cat -n "data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt" | head -100

Repository: SPOTeam/Android-V2

Length of output: 1177


🏁 Script executed:

#!/bin/bash
# Read the StudyRepositoryImpl.kt file around lines 32-46
cat -n "data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt" | sed -n '25,50p'

Repository: SPOTeam/Android-V2

Length of output: 1155


🏁 Script executed:

#!/bin/bash
# Also check StudyDataSourceImpl to see the actual API call
cat -n "data/study/src/main/java/com/umcspot/spot/study/datasourceimpl/StudyDataSourceImpl.kt" | head -150

Repository: SPOTeam/Android-V2

Length of output: 2390


🏁 Script executed:

#!/bin/bash
# Find and read the ActivityType enum
fd -type f -name "ActivityType.kt" -o -name "ActivityType.java" | head -5

Repository: SPOTeam/Android-V2

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Find and read the StudyTheme enum
fd -type f -name "StudyTheme.kt" -o -name "StudyTheme.java" | head -5

Repository: SPOTeam/Android-V2

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Find and read the FeeRange enum
fd -type f -name "FeeRange.kt" -o -name "FeeRange.java" | head -5

Repository: SPOTeam/Android-V2

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Search for usages of getRecruitingStudies to understand how it's called
rg "getRecruitingStudies" -B 2 -A 2

Repository: SPOTeam/Android-V2

Length of output: 5697


🏁 Script executed:

#!/bin/bash
# Read the complete StudyService.kt to see the full signature
cat -n "data/study/src/main/java/com/umcspot/spot/study/service/StudyService.kt" | head -80

Repository: SPOTeam/Android-V2

Length of output: 1754


🏁 Script executed:

#!/bin/bash
# Search for ActivityType, StudyTheme, FeeRange enum definitions
rg "enum class ActivityType|enum class StudyTheme|enum class FeeRange" -A 15

Repository: SPOTeam/Android-V2

Length of output: 2693


🏁 Script executed:

#!/bin/bash
# Also search in model directory
fd -name "*.kt" | xargs grep -l "enum class ActivityType\|enum class StudyTheme\|enum class FeeRange"

Repository: SPOTeam/Android-V2

Length of output: 294


null 파라미터를 API에 그대로 전달하세요

현재 코드는 null 값을 임의의 기본값으로 변환하여 실제 필터링 의도를 왜곡합니다:

  • nullOFFLINE: 사용자가 선택하지 않은 액티비티 타입이 오프라인으로만 필터됨
  • nullOTHER: 사용자가 선택하지 않은 테마가 "기타"로만 필터됨
  • nullNONE: 사용자가 선택하지 않은 비용이 무료로만 필터됨

백엔드 API(StudyService)는 이미 ActivityType?, StudyTheme?, FeeRange? 형태로 null을 지원하므로, 이 값들을 null 상태 그대로 전달하세요:

val response = studyDataSource.getRecruitingStudies(
    sortType = sortType,
    activityType = activityType,
    theme = theme,
    feeRange = feeRange
)
🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 39–43, currently nulls are being replaced with default enum values
when calling studyDataSource.getRecruitingStudies which changes intended
filtering; update the call to pass activityType, theme, and feeRange as-is
(nullable) instead of using ?: defaults so the API receives null when the user
didn't select a filter; ensure the method signature accepts ActivityType?,
StudyTheme?, FeeRange? and remove the Elvis/default operators from this call.

Comment on lines +48 to +62
override suspend fun getPreferLocationStudies(
sortType: RecruitingStudySort,
activityType: ActivityType?,
theme: StudyTheme?,
feeRange: FeeRange?
): Result<StudyResultList> =
runCatching {
val response = studyService.getRecruitingStudies(sortType = sortType, activityType = activityType, theme = theme, feeRange = feeRange)
response.result!!.toDomainList()
}.recoverCatching {
setRecommendDummies(0)
val response = studyDataSource.getRecruitingStudies(
sortType = sortType,
activityType = activityType ?: ActivityType.OFFLINE,
theme = theme ?: StudyTheme.OTHER,
feeRange = feeRange ?: FeeRange.NONE
)
response.result.toDomain()
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the StudyRepositoryImpl.kt file
find . -name "StudyRepositoryImpl.kt" -type f

Repository: SPOTeam/Android-V2

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Find StudyDataSource interface
find . -name "StudyDataSource.kt" -o -name "*StudyDataSource*" -type f

Repository: SPOTeam/Android-V2

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Read StudyRepositoryImpl.kt to see both methods
wc -l ./data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt

Repository: SPOTeam/Android-V2

Length of output: 152


🏁 Script executed:

#!/bin/bash
# Read StudyRepositoryImpl.kt content
cat -n ./data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt

Repository: SPOTeam/Android-V2

Length of output: 3288


🏁 Script executed:

#!/bin/bash
# Read StudyDataSource.kt
cat -n ./data/study/src/main/java/com/umcspot/spot/study/datasource/StudyDataSource.kt

Repository: SPOTeam/Android-V2

Length of output: 1177


getPreferLocationStudiesgetRecruitingStudies와 완전히 동일한 중복 코드 입니다

getPreferLocationStudiesgetRecruitingStudies가 정확히 같은 로직을 사용하며, 동일한 studyDataSource.getRecruitingStudies() 메서드를 호출합니다. 더욱 문제는 StudyDataSource 인터페이스에 getPreferLocationStudies() 메서드가 존재하지 않습니다.

다음 중 하나를 확인하시기 바랍니다:

  1. StudyDataSourcegetPreferLocationStudies() 메서드를 추가하고 이를 호출해야 하는가?
  2. 두 메서드가 실제로 동일한 동작을 하도록 설계된 것이라면, 공통 private 메서드로 추출하거나 하나로 통합해야 합니다.
🤖 Prompt for AI Agents
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
lines 48-62: getPreferLocationStudies duplicates getRecruitingStudies and calls
a StudyDataSource method that doesn't exist for "prefer location"; either add
getPreferLocationStudies to StudyDataSource and implement it in the data source,
or remove the duplicate by consolidating logic: extract the shared call into a
single private helper (or have getPreferLocationStudies delegate to
getRecruitingStudies) and update callers accordingly; also update the
StudyDataSource interface and implementations if you choose to introduce a new
method.

Comment on lines +55 to +61
val response = studyDataSource.getRecruitingStudies(
sortType = sortType,
activityType = activityType ?: ActivityType.OFFLINE,
theme = theme ?: StudyTheme.OTHER,
feeRange = feeRange ?: FeeRange.NONE
)
response.result.toDomain()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

response.result null 체크 필수

response.result가 null인 경우 toDomain() 호출 시 NPE가 발생합니다.

🔎 제안된 수정
         val response = studyDataSource.getRecruitingStudies(
             sortType = sortType,
             activityType = activityType ?: ActivityType.OFFLINE,
             theme = theme ?: StudyTheme.OTHER,
             feeRange = feeRange ?: FeeRange.NONE
         )
-        response.result.toDomain()
+        response.result?.toDomain() ?: throw Exception("선호 지역 스터디 결과를 받지 못했습니다")
🤖 Prompt for AI Agents
In
data/study/src/main/java/com/umcspot/spot/study/repositoryimpl/StudyRepositoryImpl.kt
around lines 55 to 61, response.result can be null and calling toDomain() will
cause an NPE; add a null check on response.result and handle it (e.g., if null
return an empty domain list or an appropriate default/empty response, otherwise
call toDomain() on the non-null value) so the method never dereferences a null
result.

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

Labels

🍒 [FEAT] 새로운 기능 구현 🐶 종훈

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT/#18] StudyRegister API 연동

1 participant

Comments