Skip to content

[Feat] Storage Presigned URL API 정의#205

Merged
kimjw2003 merged 5 commits into
developfrom
FLT-8-프로필-이미지-등록-기능
May 12, 2026

Hidden character warning

The head ref may contain hidden characters: "FLT-8-\ud504\ub85c\ud544-\uc774\ubbf8\uc9c0-\ub4f1\ub85d-\uae30\ub2a5"
Merged

[Feat] Storage Presigned URL API 정의#205
kimjw2003 merged 5 commits into
developfrom
FLT-8-프로필-이미지-등록-기능

Conversation

@kimjw2003
Copy link
Copy Markdown
Contributor

@kimjw2003 kimjw2003 commented May 12, 2026

📮 관련 이슈

  • FLT-8

📌 작업 내용

  • Storage Presigned URL 발급 API 정의 (GET /api/v1/storage/presigned-url)

생성 파일

레이어 파일
data/api StorageApi.kt
data/dto PresignedUrlResponseDto.kt
domain/type StoragePathType.kt, FileExtension.kt
domain/model PresignedUrlModel.kt
domain/mapper PresignedUrlMapper.kt
domain/repository StorageRepository.kt
data/di ServiceModule.ktprovideStorageApi 추가

사용 예시

storageRepository.getPresignedUrl(
    pathType = StoragePathType.USER_PROFILE,
    extension = FileExtension.JPG,
)

😅 미구현

  • signedUrl에 실제 이미지 업로드 연동 (ViewModel 레벨)

🫛 To. 리뷰어

Summary by CodeRabbit

릴리스 노트

  • 신기능

    • 온보딩 중 프로필 이미지 선택 및 편집 기능 추가 (갤러리에서 선택 또는 삭제 가능)
    • 온보딩 흐름에 이용약관 동의 화면 추가
  • 변경사항

    • 기본 프로필 아바타 외관 업데이트

Review Change Stack

kimjw2003 and others added 5 commits May 1, 2026 00:21
- Profile → Terms → Content 순서로 온보딩 흐름 변경
- OnboardingTermsScreen 약관 항목 확장 영역 UI 개선 (구분선 full-width, 설명 영역 배경 분리)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

📝 Walkthrough

전체 요약

프로필 이미지 선택 기능을 갖춘 온보딩 약관 스크린을 추가하고, Presigned URL을 요청할 수 있는 저장소 API 계층을 구현합니다. 온보딩 플로우는 프로필 → 약관 → 콘텐츠 순서로 확장됩니다.

변경 사항

Presigned URL 저장소 API 계층

레이어 / 파일(s) 설명
저장소 API 계약 및 응답 DTO
app/src/main/java/com/flint/data/api/StorageApi.kt, app/src/main/java/com/flint/data/dto/storage/response/PresignedUrlResponseDto.kt
Retrofit 기반 StorageApi 인터페이스가 /api/v1/storage/presigned-url 엔드포인트를 정의하고, pathTypeextension 쿼리 파라미터를 받아 PresignedUrlResponseDto를 반환합니다.
저장소 도메인 모델 및 매핑
app/src/main/java/com/flint/domain/model/storage/PresignedUrlModel.kt, app/src/main/java/com/flint/domain/mapper/storage/PresignedUrlMapper.kt, app/src/main/java/com/flint/domain/type/FileExtension.kt, app/src/main/java/com/flint/domain/type/StoragePathType.kt
PresignedUrlModel 데이터 클래스를 정의하고, DTO를 모델로 변환하는 toModel() 확장 함수를 제공합니다. 지원되는 파일 유형(JPG, PNG, PDF 등)과 저장소 경로 유형(USER_PROFILE, LOGO_IMAGE 등)을 열거형으로 정의합니다.
저장소 Repository 및 DI 설정
app/src/main/java/com/flint/domain/repository/StorageRepository.kt, app/src/main/java/com/flint/data/di/ServiceModule.kt
StorageRepository는 도메인 타입을 API 호출로 변환하고 Result 타입으로 래핑합니다. ServiceModule@Provides 메서드로 StorageApi 싱글톤을 생성합니다.

온보딩 플로우 확장

레이어 / 파일(s) 설명
프로필 이미지 기본 아바타 변경
app/src/main/java/com/flint/core/designsystem/component/image/ProfileImage.kt
ProfileImage 컴포넌트의 기본, 플레이스홀더, 오류 상태에서 사용하는 드로어블을 회색 아바타(ic_avatar_gray)로 변경합니다.
온보딩 약관 스크린
app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt
새로운 온보딩 약관 스크린을 구현하여, "전체 동의" 토글, 개별 약관 체크박스, 확장/축소 가능한 약관 설명, 그리고 "자세히 보기" 링크를 지원합니다. 모든 약관에 동의했을 때만 "동의하기" 버튼이 활성화됩니다.
온보딩 라우팅 확장
app/src/main/java/com/flint/core/navigation/Route.kt, app/src/main/java/com/flint/presentation/onboarding/navigation/OnboardingNavigation.kt
Route.OnboardingTerms 라우트 변형을 추가하고, 온보딩 네비게이션 그래프에 약관 스크린을 등록합니다. 프로필 스크린의 다음 단계를 약관 스크린으로 연결합니다.
온보딩 프로필 이미지 선택
app/src/main/java/com/flint/presentation/onboarding/OnboardingProfileScreen.kt
프로필 이미지 편집을 위해 EditProfileImage 컴포넌트를 도입하고, 갤러리 이미지 선택기 런처를 추가합니다. 하단 시트에서 갤러리에서 선택하거나 현재 사진을 삭제하는 옵션을 제공합니다.

관련 PR

  • imflint/Flint-Android#120: 동일한 ProfileImage 컴포넌트를 수정하므로 기존 변경사항과 상충할 수 있습니다.
  • imflint/Flint-Android#142: ProfileImage 리팩토링과 아바타 드로어블 변경이 겹치므로 통합이 필요할 수 있습니다.
  • imflint/Flint-Android#155: 동일한 Route 인터페이스에 새로운 온보딩 라우트 변형을 추가하므로 충돌할 수 있습니다.

제안 라벨

Feat ✨, 🔖 API

제안 검수자

  • nahy-512

리뷰 예상 난이도

🎯 3 (Moderate) | ⏱️ ~25 minutes

이 PR은 새로운 저장소 API 계층(데이터 흐름, 도메인 매핑, 의존성 주입)과 새로운 온보딩 약관 스크린(UI 상태 관리, 라우팅 통합) 및 프로필 이미지 선택 기능을 포함하여 여러 계층에 걸친 일관된 변경사항들을 다룹니다. 각 부분의 논리가 명확하지만 파일 수와 기능 범위로 인해 중간 정도의 검토 노력이 필요합니다.


🐰 회색 아바타 입고, 약관 동의하고,
이미지 선택해서 프로필 완성!
저장소 API 몸 얹고
온보딩 플로우 쭉쭉 진행되네! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 주요 변경사항을 명확하게 요약하고 있으며, Storage Presigned URL API 정의라는 핵심 내용을 잘 나타냅니다.
Description check ✅ Passed 설명이 필수 섹션(관련 이슈, 작업 내용)을 포함하며 구체적인 생성 파일 목록과 사용 예시를 제공합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FLT-8-프로필-이미지-등록-기능

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Copy Markdown
Contributor

@ckals413 ckals413 left a comment

Choose a reason for hiding this comment

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

머지합시다

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (4)
app/src/main/java/com/flint/domain/repository/StorageRepository.kt (1)

21-22: ⚡ Quick win

enum.name 직접 전송 대신 wire value를 명시적으로 분리해 주세요.

Line 21, Line 22처럼 name에 의존하면 enum 리네임만으로 서버 계약이 깨질 수 있습니다. API 전송값은 enum 식별자와 분리해 두는 편이 안전합니다.

변경 제안
// app/src/main/java/com/flint/domain/type/FileExtension.kt
-enum class FileExtension {
-    JPG,
-    JPEG,
-    PNG,
-    GIF,
-    WEBP,
-    SVG,
-    PDF,
+enum class FileExtension(val wireValue: String) {
+    JPG("JPG"),
+    JPEG("JPEG"),
+    PNG("PNG"),
+    GIF("GIF"),
+    WEBP("WEBP"),
+    SVG("SVG"),
+    PDF("PDF"),
 }

// app/src/main/java/com/flint/domain/type/StoragePathType.kt
-enum class StoragePathType {
-    USER_PROFILE,
-    LOGO_IMAGE,
-    COLLECTION_THUMBNAIL,
-    COLLECTION_CONTENT,
+enum class StoragePathType(val wireValue: String) {
+    USER_PROFILE("USER_PROFILE"),
+    LOGO_IMAGE("LOGO_IMAGE"),
+    COLLECTION_THUMBNAIL("COLLECTION_THUMBNAIL"),
+    COLLECTION_CONTENT("COLLECTION_CONTENT"),
 }

// app/src/main/java/com/flint/domain/repository/StorageRepository.kt
             api.getPresignedUrl(
-                pathType = pathType.name,
-                extension = extension.name,
+                pathType = pathType.wireValue,
+                extension = extension.wireValue,
             ).data.toModel()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/flint/domain/repository/StorageRepository.kt` around
lines 21 - 22, Don't send enum.name directly; instead send explicit wire values
for the enums used when building the payload in StorageRepository (currently
where pathType = pathType.name and extension = extension.name). Replace those
usages to map the enums to stable wire strings (e.g., add a property or function
on the enum like toWireValue() or a `val wireValue` and use pathType.wireValue
and extension.wireValue, or a when(...) mapper) so renaming enum constants won't
break the API contract.
app/src/main/java/com/flint/presentation/onboarding/navigation/OnboardingNavigation.kt (1)

51-59: ⚡ Quick win

사용되지 않는 sharedViewModel

Line 52에서 sharedViewModel을 가져오지만 OnboardingTermsRoute에 전달하지 않습니다. 다른 온보딩 화면들(예: OnboardingProfileRoute line 68, OnboardingContentRoute line 79)은 모두 viewModel을 전달하는 반면, 약관 동의 화면은 공유 상태가 필요하지 않은 것으로 보입니다.

약관 동의 화면이 공유 ViewModel이 필요하지 않다면 line 52의 sharedViewModel 선언을 제거하는 것이 좋습니다.

♻️ 제안된 수정
 composable<Route.OnboardingTerms> { backStackEntry ->
-    val sharedViewModel = backStackEntry.sharedViewModel<OnboardingViewModel>(navController)
-
     OnboardingTermsRoute(
         paddingValues = paddingValues,
         navigateUp = navController::navigateUp,
         navigateToOnboardingContent = navController::navigateToOnboardingContent,
     )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/flint/presentation/onboarding/navigation/OnboardingNavigation.kt`
around lines 51 - 59, Remove the unused sharedViewModel declaration in the
composable for Route.OnboardingTerms: the local variable sharedViewModel
(created via backStackEntry.sharedViewModel<OnboardingViewModel>(navController))
is not passed into OnboardingTermsRoute and appears unnecessary, so delete that
declaration from the composable block to avoid dead code; if the terms screen
later needs shared state, pass the same sharedViewModel into
OnboardingTermsRoute as done for OnboardingProfileRoute and
OnboardingContentRoute.
app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt (2)

46-60: ⚡ Quick win

상세보기 URL이 비어있음

두 약관 항목의 detailUrl이 모두 빈 문자열입니다. 이로 인해 사용자가 "자세히 보기"(line 228)를 클릭해도 아무 동작도 수행되지 않습니다. 실제 약관 상세 URL이 준비되기 전까지는 해당 링크를 숨기거나 비활성화하는 것을 고려해보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt`
around lines 46 - 60, The two TermItem entries in the terms list have empty
detailUrl values, so the "자세히 보기" link becomes a no-op; update the code to
either provide real URLs or guard the UI: in the OnboardingTermsScreen where you
render each TermItem and the "자세히 보기" action, check TermItem.detailUrl for
null/empty and if empty either hide the "자세히 보기" button or render it disabled
(and prevent navigation in the click handler), or populate detailUrl with the
correct agreement URLs; reference the terms list and TermItem.detailUrl and the
click handler that opens the detail view to implement the guard.

144-144: 💤 Low value

상태 업데이트 패턴 개선 제안

toMutableList().also { it[index] = !it[index] } 패턴은 동작하지만, Kotlin의 컬렉션 변환 함수를 사용하면 더 간결합니다.

♻️ 더 간결한 구현 제안
-checkedStates = checkedStates.toMutableList().also { it[index] = !it[index] }
+checkedStates = checkedStates.mapIndexed { i, checked -> if (i == index) !checked else checked }

동일하게 line 147에도 적용할 수 있습니다:

-expandedStates = expandedStates.toMutableList().also { it[index] = !it[index] }
+expandedStates = expandedStates.mapIndexed { i, expanded -> if (i == index) !expanded else expanded }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt`
at line 144, Replace the mutable-copy pattern used to toggle an item in the
checkedStates list (the expression using toMutableList().also { it[index] =
!it[index] } in OnboardingTermsScreen) with an immutable collection
transformation using mapIndexed to produce a new list where the value at the
target index is inverted and all other values are preserved; apply the same
mapIndexed-based change to the other occurrence (the similar update at the other
toggle site) so state updates are concise and idiomatic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/com/flint/presentation/onboarding/OnboardingProfileScreen.kt`:
- Around line 229-233: 현재 galleryLauncher.launch(...)를 호출하는 clickAction 핸들러에서
바텀시트를 닫는 처리가 빠져 있습니다; clickAction(예: 해당 블록에서 사용 중인 galleryLauncher,
PickVisualMediaRequest 및 ActivityResultContracts.PickVisualMedia.ImageOnly 호출)
실행 직후 showProfileBottomSheet = false를 설정하여 바텀시트를 명시적으로 닫아 주세요. 동일한 패턴이 존재하는 다른
clickAction(예: 라인 236-239 범위)에도 동일한 showProfileBottomSheet = false 추가를 적용해 주세요.

---

Nitpick comments:
In `@app/src/main/java/com/flint/domain/repository/StorageRepository.kt`:
- Around line 21-22: Don't send enum.name directly; instead send explicit wire
values for the enums used when building the payload in StorageRepository
(currently where pathType = pathType.name and extension = extension.name).
Replace those usages to map the enums to stable wire strings (e.g., add a
property or function on the enum like toWireValue() or a `val wireValue` and use
pathType.wireValue and extension.wireValue, or a when(...) mapper) so renaming
enum constants won't break the API contract.

In
`@app/src/main/java/com/flint/presentation/onboarding/navigation/OnboardingNavigation.kt`:
- Around line 51-59: Remove the unused sharedViewModel declaration in the
composable for Route.OnboardingTerms: the local variable sharedViewModel
(created via backStackEntry.sharedViewModel<OnboardingViewModel>(navController))
is not passed into OnboardingTermsRoute and appears unnecessary, so delete that
declaration from the composable block to avoid dead code; if the terms screen
later needs shared state, pass the same sharedViewModel into
OnboardingTermsRoute as done for OnboardingProfileRoute and
OnboardingContentRoute.

In
`@app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt`:
- Around line 46-60: The two TermItem entries in the terms list have empty
detailUrl values, so the "자세히 보기" link becomes a no-op; update the code to
either provide real URLs or guard the UI: in the OnboardingTermsScreen where you
render each TermItem and the "자세히 보기" action, check TermItem.detailUrl for
null/empty and if empty either hide the "자세히 보기" button or render it disabled
(and prevent navigation in the click handler), or populate detailUrl with the
correct agreement URLs; reference the terms list and TermItem.detailUrl and the
click handler that opens the detail view to implement the guard.
- Line 144: Replace the mutable-copy pattern used to toggle an item in the
checkedStates list (the expression using toMutableList().also { it[index] =
!it[index] } in OnboardingTermsScreen) with an immutable collection
transformation using mapIndexed to produce a new list where the value at the
target index is inverted and all other values are preserved; apply the same
mapIndexed-based change to the other occurrence (the similar update at the other
toggle site) so state updates are concise and idiomatic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07575e41-4d79-4081-a86a-62d7699be174

📥 Commits

Reviewing files that changed from the base of the PR and between 62e570f and 9745aa0.

📒 Files selected for processing (13)
  • app/src/main/java/com/flint/core/designsystem/component/image/ProfileImage.kt
  • app/src/main/java/com/flint/core/navigation/Route.kt
  • app/src/main/java/com/flint/data/api/StorageApi.kt
  • app/src/main/java/com/flint/data/di/ServiceModule.kt
  • app/src/main/java/com/flint/data/dto/storage/response/PresignedUrlResponseDto.kt
  • app/src/main/java/com/flint/domain/mapper/storage/PresignedUrlMapper.kt
  • app/src/main/java/com/flint/domain/model/storage/PresignedUrlModel.kt
  • app/src/main/java/com/flint/domain/repository/StorageRepository.kt
  • app/src/main/java/com/flint/domain/type/FileExtension.kt
  • app/src/main/java/com/flint/domain/type/StoragePathType.kt
  • app/src/main/java/com/flint/presentation/onboarding/OnboardingProfileScreen.kt
  • app/src/main/java/com/flint/presentation/onboarding/OnboardingTermsScreen.kt
  • app/src/main/java/com/flint/presentation/onboarding/navigation/OnboardingNavigation.kt

Comment on lines +229 to +233
clickAction = {
galleryLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

메뉴 액션 이후 바텀시트를 명시적으로 닫아주세요.

Line 229, Line 238의 clickAction에서 showProfileBottomSheet = false 처리가 없어 바텀시트가 남을 수 있습니다. 액션 직후 닫기를 명시하는 편이 안전합니다.

수정 예시
                     MenuBottomSheetData(
                         label = "갤러리에서 선택",
                         clickAction = {
+                            showProfileBottomSheet = false
                             galleryLauncher.launch(
                                 PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
                             )
                         }
                     ),
                     MenuBottomSheetData(
                         label = "프로필 사진 삭제",
                         color = FlintTheme.colors.error500,
-                        clickAction = { selectedImageUri = null }
+                        clickAction = {
+                            selectedImageUri = null
+                            showProfileBottomSheet = false
+                        }
                     ),

Also applies to: 236-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/flint/presentation/onboarding/OnboardingProfileScreen.kt`
around lines 229 - 233, 현재 galleryLauncher.launch(...)를 호출하는 clickAction 핸들러에서
바텀시트를 닫는 처리가 빠져 있습니다; clickAction(예: 해당 블록에서 사용 중인 galleryLauncher,
PickVisualMediaRequest 및 ActivityResultContracts.PickVisualMedia.ImageOnly 호출)
실행 직후 showProfileBottomSheet = false를 설정하여 바텀시트를 명시적으로 닫아 주세요. 동일한 패턴이 존재하는 다른
clickAction(예: 라인 236-239 범위)에도 동일한 showProfileBottomSheet = false 추가를 적용해 주세요.

@kimjw2003 kimjw2003 merged commit 9620fb3 into develop May 12, 2026
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 13, 2026
1 task
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.

2 participants