Skip to content

[feat] #179 #180 #181 #184 Sprint 4: 사진 이동/복제 + 앨범 삭제#187

Open
ikseong00 wants to merge 20 commits intodevelopfrom
feat/4th-sprint
Open

[feat] #179 #180 #181 #184 Sprint 4: 사진 이동/복제 + 앨범 삭제#187
ikseong00 wants to merge 20 commits intodevelopfrom
feat/4th-sprint

Conversation

@ikseong00
Copy link
Copy Markdown
Contributor

@ikseong00 ikseong00 commented Apr 11, 2026

🔗 관련 이슈

📙 작업 설명

🧪 테스트 내역 (선택)

📸 스크린샷 또는 시연 영상 (선택)

기능 미리보기 기능 미리보기
#180 PhotoDetail 앨범에 추가
default.mp4
#179 AllPhoto 사진 복제
default.mp4
#181 AlbumDetail 사진 이동/복제
default.mp4
#184 AlbumDetail 앨범 삭제
default.mp4
#182 AlbumDetail 사진 가져오기
default.mp4
SelectAlbum 출처 앨범 비활성화
default.mp4

💬 추가 설명 or 리뷰 포인트 (선택)

  • SelectAlbumAction sealed interface로 upload/move/copy 액션을 통합 처리
  • PhotoMovedResult ResultBus 이벤트로 AlbumDetail 사진 목록 새로고침 트리거
  • 사진 가져오기 바텀시트: ModalBottomSheet + contentWindowInsets = { WindowInsets() } 로 statusBar 아래 전체 높이 구현
  • 이동 SelectAlbum: disabledAlbumId = sourceFolderId 로 출처 앨범 UI/클릭 비활성화
  • 추후 토스트 액션 버튼 기능 추가 필요 (현재 미구현)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 앨범 간 사진 복사 및 이동 기능 추가
    • 갤러리에서 앨범으로 사진 가져오기 기능 추가
    • 앨범 삭제 기능 추가
    • 사진 상세 화면에서 앨범 추가 옵션 추가
  • 개선 사항

    • 사진 작업 UI 및 아이콘 업데이트
    • 사진 선택 및 관리 기능 강화

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0cbb9079-e296-4bd4-9540-51108698b6b7

📥 Commits

Reviewing files that changed from the base of the PR and between afa5bd8 and 65accbf.

📒 Files selected for processing (3)
  • core/ui/src/main/java/com/neki/android/core/ui/component/DropdownPopup.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt

Walkthrough

사진 복사·이동 API와 저장소 메서드가 추가되고, 앨범 상세·전체 사진·선택 앨범 UI/네비게이션에 복사/이동 및 앨범 삭제·가져오기 플로우가 통합되었습니다.

Changes

Cohort / File(s) Summary
데이터 API / 서비스
core/data-api/src/main/java/com/neki/android/core/dataapi/repository/FolderRepository.kt, core/data/src/main/java/com/neki/android/core/data/remote/api/FolderService.kt, core/data/src/main/java/com/neki/android/core/data/remote/model/request/MovePhotosRequest.kt, core/data/src/main/java/com/neki/android/core/data/remote/model/request/CopyPhotosRequest.kt
폴더/사진 대상 복사·이동을 위한 repository 메서드(movePhotos, copyPhotos)와 원격 API 엔드포인트 및 요청 모델(MovePhotosRequest, CopyPhotosRequest) 추가.
저장소 구현
core/data/src/main/java/com/neki/android/core/data/repository/impl/FolderRepositoryImpl.kt
새 API 호출을 사용해 movePhotoscopyPhotos 오버라이드 추가, 호출을 runSuspendCatching으로 래핑하여 Result<Unit> 반환.
앨범 상세 기능
feature/archive/impl/src/main/kotlin/.../album_detail/AlbumDetailContract.kt, .../AlbumDetailScreen.kt, .../AlbumDetailViewModel.kt, .../component/AlbumDetailActionBar.kt, .../component/AlbumDetailTopBar.kt, .../component/ImportPhotoBottomSheet.kt, .../component/ImportPhotoGridItem.kt
앨범 삭제, 사진 가져오기(Import) UI·상태·의도·사이드이펙트 추가; 복사/이동 흐름 및 삭제 바텀시트/토스트/내비게이션 구현; Import 바닥시트와 그리드 아이템 추가.
전체 사진 / 사진 상세 기능
feature/archive/impl/src/main/kotlin/.../photo/AllPhotoContract.kt, .../AllPhotoScreen.kt, .../AllPhotoViewModel.kt, .../photo_detail/PhotoDetailContract.kt, .../PhotoDetailScreen.kt, .../PhotoDetailViewModel.kt
전체사진 및 사진상세에 '복사' 액션과 앨범 선택 네비게이션 연결, 옵션 팝업 및 관련 사이드이펙트 추가.
선택 앨범(Select Album)
feature/select-album/api/src/main/java/.../SelectAlbumAction.kt, .../SelectAlbumNavKey.kt, feature/select-album/impl/src/main/java/.../SelectAlbumContract.kt, .../SelectAlbumScreen.kt, .../SelectAlbumViewModel.kt
CopyPhotos/MovePhotos 액션 추가, 기본 multiSelect 값 변경, disabledAlbumId 지원, 결과 전송(SendPhotoMovedResult, SendPhotoCopiedResult) 및 다중 대상 처리 로직 확장.
네비게이션 및 결과 전파
feature/archive/impl/src/main/kotlin/.../navigation/ArchiveEntryProvider.kt, feature/archive/impl/src/main/kotlin/.../main/ArchiveMainScreen.kt
사진 복사/이동 결과 처리(페이지별 결과 효과 추가) 및 navigateToSelectAlbum 경로 연결, 일부 레이아웃 패딩 조정.
디자인 시스템 / UI 컴포넌트 변경
core/designsystem/src/main/java/.../actionbar/NekiActionBar.kt, core/ui/src/main/java/.../component/AlbumRowComponent.kt, core/ui/src/main/java/.../component/DropdownPopup.kt
기존 액션바 재구성(NekiActionBar 도입, 시그니처 변경), 앨범 행의 비활성화 시각/클릭 무시 지원, 드롭다운에 항목별 텍스트 색상 옵션 추가.
리소스 및 사소한 앱 UI 수정
core/designsystem/src/main/res/drawable/icon_copy_photo.xml, feature/archive/impl/src/main/res/drawable/icon_copy_photo.xml, feature/archive/impl/src/main/res/drawable/icon_move_photo.xml, app/src/main/java/.../AddPhotoBottomSheet.kt, app/src/main/java/.../MainScreen.kt, feature/archive/impl/build.gradle.kts
복사/이동 아이콘 벡터 추가(디자인시스템·피처 레벨), 작은 리소스/아이콘 교체, 빌드 의존성 추가 및 경미한 포맷 변경.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as AlbumDetail UI
    participant VM as AlbumDetailViewModel
    participant Repo as FolderRepository
    participant API as FolderService
    participant Server as Backend

    User->>UI: 사진 선택 후 '복사' 또는 '이동' 클릭
    UI->>VM: ClickCopyIcon / ClickMoveIcon (selected photoIds)
    VM->>VM: selectedPhotos 검증
    VM->>UI: NavigateToSelectAlbum(SelectAlbumAction.CopyPhotos/MovePhotos)
    UI->>User: 앨범 선택 화면 표시

    User->>UI: 대상 앨범 선택 & 확인
    UI->>SelectAlbumVM: ClickConfirmButton (selected albums)
    SelectAlbumVM->>Repo: copyPhotos(photoIds, targetFolderIds) or movePhotos(sourceFolderId, photoIds, targetFolderIds)
    Repo->>API: copyPhotos / movePhotos 요청 (Copy/MovePhotosRequest)
    API->>Server: HTTP POST/PATCH /api/folders/photos/...
    Server-->>API: 응답 Result<Unit>
    API-->>Repo: Result<Unit>
    Repo-->>SelectAlbumVM: Result<Unit>
    SelectAlbumVM->>UI: SendPhotoCopiedResult / SendPhotoMovedResult (resultEventBus)
    UI->>User: 성공 토스트 및 네비게이션(뒤로)
Loading
sequenceDiagram
    actor User
    participant UI as AlbumDetail UI
    participant VM as AlbumDetailViewModel
    participant Repo as FolderRepository
    participant API as FolderService
    participant Server as Backend

    User->>UI: 앨범 옵션에서 '앨범 삭제' 선택
    UI->>VM: ClickDeleteAlbumOption
    VM->>UI: isShowDeleteAlbumBottomSheet = true
    User->>UI: 삭제 옵션 선택 (사진 포함/미포함) & 확인
    UI->>VM: ClickDeleteAlbumConfirmButton (deletePhotos)
    VM->>Repo: deleteFolder(albumId, deletePhotos)
    Repo->>API: DELETE /api/folders/{id}
    API->>Server: DELETE 요청
    Server-->>API: Result<Unit>
    API-->>Repo: Result<Unit>
    Repo-->>VM: Result<Unit>
    VM->>UI: NavigateBack / NotifyResult / 토스트
    UI->>User: 앨범 목록으로 이동 및 성공 알림
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 새 앨범에 사진을 실어,
복사도 옮김도 척척 해,
바닥시트 열고 선택하면,
토끼가 살짝 도와줄게요,
퐁당, 앨범이 반짝 빛나네 ✨📸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% 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 PR 제목은 Sprint 4의 주요 기능들(사진 이동/복제, 앨범 삭제)을 명확하게 요약하고 있으며 관련 이슈 번호를 명시하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿을 완전히 따르고 있으며, 관련 이슈, 작업 설명, 테스트 내역, 스크린샷, 추가 설명이 모두 충실하게 포함되어 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 모든 연결된 이슈(#179, #180, #181, #182, #184)의 요구사항을 충족합니다. 사진 이동/복제 API, 선택 UI 흐름, 앨범 삭제 기능이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 연결된 이슈들의 범위 내에 있습니다. 레이아웃 조정, 리소스 추가, API 확장 등 모두 명시된 기능 구현과 관련이 있습니다.

✏️ 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 feat/4th-sprint

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.

@ikseong00 ikseong00 changed the base branch from feat/#178-select-album to develop April 13, 2026 04:31
@ikseong00 ikseong00 marked this pull request as ready for review April 14, 2026 04:20
@github-actions github-actions bot requested a review from Ojongseok April 14, 2026 04:21
Copy link
Copy Markdown

@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

🧹 Nitpick comments (3)
feature/archive/impl/src/main/res/drawable/icon_copy_photo.xml (1)

1-33: 아이콘 색상 일관성 확인 필요

이 아이콘은 스트로크 색상 #616575를 사용하지만, 같은 PR에서 추가된 icon_move_photo.xml#B7B9C3을 사용합니다. 의도된 디자인 차이인지 확인해 주세요.

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

In `@feature/archive/impl/src/main/res/drawable/icon_copy_photo.xml` around lines
1 - 33, 아이콘 색상 불일치 문제: 이 파일의 벡터 요소들(그룹 내부의 <path>와 외부의 세 <path> 요소들에서 사용하는
android:strokeColor="#616575")이 같은 PR의 다른 아이콘(icon_move_photo.xml)이 사용하는
"#B7B9C3"과 다릅니다; 디자인 의도에 따라 일관되게 만들려면 해당 요소들의 android:strokeColor 값을 프로젝트 표준
색("#B7B9C3" 또는 지정된 디자인 색상)으로 변경하거나(또는 반대로 icon_move_photo.xml을 "#616575"로 수정)
디자이너/디자인 토큰을 확인해 결정하고 모든 관련 path 요소(android:strokeColor가 설정된 요소들)에서 동일한 색상으로
통일하세요.
feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailActionBar.kt (1)

81-110: ActionBarItem 컴포넌트가 AllPhotoActionBar.kt와 중복됩니다.

이 파일의 ActionBarItemAllPhotoActionBar.ktActionBarItem이 동일한 구현을 가지고 있습니다. 공통 컴포넌트로 추출하여 재사용하는 것을 고려해 주세요.

♻️ 공통 컴포넌트 추출 제안

ActionBarItemcore/ui 또는 feature/archive/impl/component 등 공통 위치로 이동하여 두 파일에서 재사용할 수 있습니다.

// 공통 위치에 ActionBarItem.kt 생성
`@Composable`
fun ActionBarItem(
    iconRes: Int,
    label: String,
    isEnabled: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    // 기존 구현...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailActionBar.kt`
around lines 81 - 110, ActionBarItem is duplicated between
AlbumDetailActionBar.kt and AllPhotoActionBar.kt; extract the `@Composable`
ActionBarItem function into a shared file (e.g., core/ui or
feature/archive/impl/component/ActionBarItem.kt), keep its signature (iconRes,
label, isEnabled, onClick, modifier) and implementation identical, then replace
the local definitions in AlbumDetailActionBar and AllPhotoActionBar with imports
of the shared ActionBarItem and remove the duplicated function declarations;
ensure package and import statements are updated so both AlbumDetailActionBar
and AllPhotoActionBar reference the new shared component.
feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt (1)

180-194: density 변수가 중복 선언되어 있습니다.

Line 181의 density 변수는 Line 118에서 이미 선언된 동일한 이름의 변수를 섀도잉합니다. 외부 스코프의 density를 재사용하거나, 명확성을 위해 다른 이름을 사용하세요.

♻️ 중복 선언 제거 제안
    if (uiState.isShowOptionPopup) {
-       val density = LocalDensity.current
-       val popupOffsetX = with(density) { (-20).dp.toPx().toInt() }
-       val popupOffsetY = with(density) { 54.dp.toPx().toInt() }
+       val popupOffsetX = with(LocalDensity.current) { (-20).dp.toPx().toInt() }
+       val popupOffsetY = with(LocalDensity.current) { 54.dp.toPx().toInt() }

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

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt`
around lines 180 - 194, The LocalDensity.current variable is being shadowed:
remove the inner declaration at the PhotoDetailScreen block and reuse the
existing density from the outer scope (or rename consistently) so popupOffsetX
and popupOffsetY compute using that same density; update the computations that
set popupOffsetX and popupOffsetY (used by DropdownPopup) to reference the
existing density variable, leaving the rest (uiState.isShowOptionPopup,
DropdownPopup, onIntent handlers, and PhotoDetailIntent values) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt`:
- Around line 114-117: When handling AlbumDetailIntent.SelectImportAlbum in
AlbumDetailViewModel, you're only updating importPhotoState.selectedAlbumId but
not clearing previous importPhotoState.selectedPhotoIds (and related selection
flags), so previously selected photo IDs from another album are retained and
later sent by loadImportPhotos; fix by clearing
importPhotoState.selectedPhotoIds (and any isAllSelected/isSelectAll flags) in
the same reduce call that sets selectedAlbumId and isShowAlbumDropdown before
calling loadImportPhotos so selection state is reset whenever selectedAlbumId
changes.
- Around line 315-323: Before calling folderRepository.copyPhotos in the
viewModelScope.launch block, guard against an empty selection: check
state.importPhotoState.selectedPhotoIds (used in the current block) and if
empty, stop the flow (reset importPhotoState.isLoading = false if set, close the
bottom sheet or keep it open as desired) and optionally post a side effect like
a toast or no-op; only call folderRepository.copyPhotos when
selectedPhotoIds.isNotEmpty(). Update the logic around viewModelScope.launch /
reduce and the copyPhotos invocation to perform this early-return check so no
request is made with an empty list.
- Around line 294-296: The "전체사진" count currently uses loadedAlbums.sumOf {
it.photoCount } which double-counts photos that appear in multiple albums;
change the calculation used when constructing AlbumFilterOption (the
AlbumFilterOption(..., loadedAlbums.sumOf { it.photoCount }) call) to count
unique photos instead by flattening loadedAlbums' photo lists and taking
distinct by photo id (or using an existing combined/allPhotos collection) and
using its size so the total reflects unique photos across albums.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/ImportPhotoBottomSheet.kt`:
- Around line 71-73: The sheetHeight is stored with remember but not keyed to
changing inputs so it won't recompute on rotation or inset changes; update the
calculation of sheetHeight (the val sheetHeight using remember) to either
include configuration, statusBarHeight, and navigationBarHeight as remember keys
or remove remember and compute it directly each composition so that sheetHeight
recalculates when configuration or inset values change.

In
`@feature/select-album/api/src/main/java/com/neki/android/feature/select_album/api/SelectAlbumNavKey.kt`:
- Line 20: The default value for SelectAlbumNavKey.multiSelect was changed and
that affects callers that rely on the implicit default; update either the
default back to its original value or explicitly pass the desired boolean at
each call site: locate SelectAlbumNavKey (constructor/property multiSelect) and
the navigateToSelectAlbum usages (the functions invoking navigateToSelectAlbum
in MainActivity, MainScreen, AlbumDetailScreen, AllPhotoScreen,
PhotoDetailScreen) and ensure each call explicitly supplies
multiSelect=true/false to preserve intended behavior, or revert the multiSelect
default to its previous value to avoid changing behavior across those five
callers.

---

Nitpick comments:
In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailActionBar.kt`:
- Around line 81-110: ActionBarItem is duplicated between
AlbumDetailActionBar.kt and AllPhotoActionBar.kt; extract the `@Composable`
ActionBarItem function into a shared file (e.g., core/ui or
feature/archive/impl/component/ActionBarItem.kt), keep its signature (iconRes,
label, isEnabled, onClick, modifier) and implementation identical, then replace
the local definitions in AlbumDetailActionBar and AllPhotoActionBar with imports
of the shared ActionBarItem and remove the duplicated function declarations;
ensure package and import statements are updated so both AlbumDetailActionBar
and AllPhotoActionBar reference the new shared component.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt`:
- Around line 180-194: The LocalDensity.current variable is being shadowed:
remove the inner declaration at the PhotoDetailScreen block and reuse the
existing density from the outer scope (or rename consistently) so popupOffsetX
and popupOffsetY compute using that same density; update the computations that
set popupOffsetX and popupOffsetY (used by DropdownPopup) to reference the
existing density variable, leaving the rest (uiState.isShowOptionPopup,
DropdownPopup, onIntent handlers, and PhotoDetailIntent values) unchanged.

In `@feature/archive/impl/src/main/res/drawable/icon_copy_photo.xml`:
- Around line 1-33: 아이콘 색상 불일치 문제: 이 파일의 벡터 요소들(그룹 내부의 <path>와 외부의 세 <path>
요소들에서 사용하는 android:strokeColor="#616575")이 같은 PR의 다른 아이콘(icon_move_photo.xml)이
사용하는 "#B7B9C3"과 다릅니다; 디자인 의도에 따라 일관되게 만들려면 해당 요소들의 android:strokeColor 값을 프로젝트
표준 색("#B7B9C3" 또는 지정된 디자인 색상)으로 변경하거나(또는 반대로 icon_move_photo.xml을 "#616575"로 수정)
디자이너/디자인 토큰을 확인해 결정하고 모든 관련 path 요소(android:strokeColor가 설정된 요소들)에서 동일한 색상으로
통일하세요.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b858f24c-a94d-4bb9-8854-f5c3f9e97c07

📥 Commits

Reviewing files that changed from the base of the PR and between 067476c and afa5bd8.

📒 Files selected for processing (37)
  • app/src/main/java/com/neki/android/app/main/MainScreen.kt
  • app/src/main/java/com/neki/android/app/main/component/AddPhotoBottomSheet.kt
  • core/data-api/src/main/java/com/neki/android/core/dataapi/repository/FolderRepository.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/api/FolderService.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/model/request/CopyPhotosRequest.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/model/request/MovePhotosRequest.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/FolderRepositoryImpl.kt
  • core/designsystem/src/main/java/com/neki/android/core/designsystem/actionbar/NekiActionBar.kt
  • core/designsystem/src/main/res/drawable/icon_copy_photo.xml
  • core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt
  • feature/archive/api/src/main/kotlin/com/neki/android/feature/archive/api/ArchiveResult.kt
  • feature/archive/impl/build.gradle.kts
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailContract.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailActionBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/FavoriteAlbumActionBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/ImportPhotoBottomSheet.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/ImportPhotoGridItem.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainScreen.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/navigation/ArchiveEntryProvider.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoContract.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoViewModel.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoActionBar.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailContract.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailViewModel.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt
  • feature/archive/impl/src/main/res/drawable/icon_copy_photo.xml
  • feature/archive/impl/src/main/res/drawable/icon_move_photo.xml
  • feature/select-album/api/src/main/java/com/neki/android/feature/select_album/api/SelectAlbumAction.kt
  • feature/select-album/api/src/main/java/com/neki/android/feature/select_album/api/SelectAlbumNavKey.kt
  • feature/select-album/impl/src/main/java/com/neki/android/feature/select_album/impl/SelectAlbumContract.kt
  • feature/select-album/impl/src/main/java/com/neki/android/feature/select_album/impl/SelectAlbumScreen.kt
  • feature/select-album/impl/src/main/java/com/neki/android/feature/select_album/impl/SelectAlbumViewModel.kt

Comment on lines +114 to +117
is AlbumDetailIntent.SelectImportAlbum -> {
reduce { copy(importPhotoState = importPhotoState.copy(selectedAlbumId = intent.albumId, isShowAlbumDropdown = false)) }
loadImportPhotos(intent.albumId, reduce)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

가져올 앨범을 바꿔도 이전 선택이 유지됩니다.

Line 115에서는 selectedAlbumId만 바꾸고 있어서, 직전에 고른 selectedPhotoIds가 그대로 남습니다. 그 상태로 확인하면 Line 318이 이전 앨범의 사진 ID를 그대로 전송해서, 현재 보고 있는 앨범과 다른 사진이 가져와질 수 있습니다.

수정 예시
 is AlbumDetailIntent.SelectImportAlbum -> {
-    reduce { copy(importPhotoState = importPhotoState.copy(selectedAlbumId = intent.albumId, isShowAlbumDropdown = false)) }
+    reduce {
+        copy(
+            importPhotoState = importPhotoState.copy(
+                selectedAlbumId = intent.albumId,
+                isShowAlbumDropdown = false,
+                selectedPhotoIds = emptySet<Long>().toImmutableSet(),
+            ),
+        )
+    }
     loadImportPhotos(intent.albumId, reduce)
 }
📝 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
is AlbumDetailIntent.SelectImportAlbum -> {
reduce { copy(importPhotoState = importPhotoState.copy(selectedAlbumId = intent.albumId, isShowAlbumDropdown = false)) }
loadImportPhotos(intent.albumId, reduce)
}
is AlbumDetailIntent.SelectImportAlbum -> {
reduce {
copy(
importPhotoState = importPhotoState.copy(
selectedAlbumId = intent.albumId,
isShowAlbumDropdown = false,
selectedPhotoIds = emptySet<Long>().toImmutableSet(),
),
)
}
loadImportPhotos(intent.albumId, reduce)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt`
around lines 114 - 117, When handling AlbumDetailIntent.SelectImportAlbum in
AlbumDetailViewModel, you're only updating importPhotoState.selectedAlbumId but
not clearing previous importPhotoState.selectedPhotoIds (and related selection
flags), so previously selected photo IDs from another album are retained and
later sent by loadImportPhotos; fix by clearing
importPhotoState.selectedPhotoIds (and any isAllSelected/isSelectAll flags) in
the same reduce call that sets selectedAlbumId and isShowAlbumDropdown before
calling loadImportPhotos so selection state is reset whenever selectedAlbumId
changes.

Comment on lines +294 to +296
val options = buildList {
add(AlbumFilterOption(null, "전체사진", loadedAlbums.sumOf { it.photoCount }))
addAll(loadedAlbums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

전체사진 개수가 중복 집계될 수 있습니다.

여기서는 이미 전체 사진 목록을 불러오고 있는데, 카운트를 loadedAlbums.sumOf { it.photoCount }로 만들면 같은 사진이 여러 앨범에 복제된 경우 실제 전체 사진 수보다 크게 표시됩니다.

수정 예시
 private fun loadImportAlbumsAndPhotos(reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit) {
     viewModelScope.launch {
         var loadedAlbums: List<AlbumPreview> = emptyList()
+        var allPhotosCount = 0
         awaitAll(
             async { folderRepository.getFolders().onSuccess { albums -> loadedAlbums = albums } },
             async {
                 photoRepository.getPhotos(folderId = null).onSuccess { photos ->
+                    allPhotosCount = photos.size
                     reduce { copy(importPhotoState = importPhotoState.copy(photos = photos.toImmutableList())) }
                 }
             },
         )
         val options = buildList {
-            add(AlbumFilterOption(null, "전체사진", loadedAlbums.sumOf { it.photoCount }))
+            add(AlbumFilterOption(null, "전체사진", allPhotosCount))
             addAll(loadedAlbums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
         }.toImmutableList()
         reduce { copy(importPhotoState = importPhotoState.copy(allAlbumOptions = options)) }
     }
 }
📝 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
val options = buildList {
add(AlbumFilterOption(null, "전체사진", loadedAlbums.sumOf { it.photoCount }))
addAll(loadedAlbums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
private fun loadImportAlbumsAndPhotos(reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit) {
viewModelScope.launch {
var loadedAlbums: List<AlbumPreview> = emptyList()
var allPhotosCount = 0
awaitAll(
async { folderRepository.getFolders().onSuccess { albums -> loadedAlbums = albums } },
async {
photoRepository.getPhotos(folderId = null).onSuccess { photos ->
allPhotosCount = photos.size
reduce { copy(importPhotoState = importPhotoState.copy(photos = photos.toImmutableList())) }
}
},
)
val options = buildList {
add(AlbumFilterOption(null, "전체사진", allPhotosCount))
addAll(loadedAlbums.map { AlbumFilterOption(it.id, it.title, it.photoCount) })
}.toImmutableList()
reduce { copy(importPhotoState = importPhotoState.copy(allAlbumOptions = options)) }
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt`
around lines 294 - 296, The "전체사진" count currently uses loadedAlbums.sumOf {
it.photoCount } which double-counts photos that appear in multiple albums;
change the calculation used when constructing AlbumFilterOption (the
AlbumFilterOption(..., loadedAlbums.sumOf { it.photoCount }) call) to count
unique photos instead by flattening loadedAlbums' photo lists and taking
distinct by photo id (or using an existing combined/allPhotos collection) and
using its size so the total reflects unique photos across albums.

Comment on lines +315 to +323
viewModelScope.launch {
reduce { copy(importPhotoState = importPhotoState.copy(isLoading = true)) }
folderRepository.copyPhotos(
photoIds = state.importPhotoState.selectedPhotoIds.toList(),
targetFolderIds = listOf(albumId),
).onSuccess {
reduce { copy(isShowImportPhotoBottomSheet = false, importPhotoState = ImportPhotoState()) }
postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 앨범에 추가했어요"))
postSideEffect(AlbumDetailSideEffect.PhotoImported(albumId))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

선택된 사진이 없을 때는 가져오기 API를 막아주세요.

현재는 빈 selectedPhotoIds도 그대로 copyPhotos(...)로 전달됩니다. UI에서 버튼을 비활성화하더라도 ViewModel 쪽 방어가 없으면 불필요한 요청이나 애매한 실패 토스트가 발생할 수 있습니다.

수정 예시
 private fun handleConfirmImport(
     state: AlbumDetailState,
     reduce: (AlbumDetailState.() -> AlbumDetailState) -> Unit,
     postSideEffect: (AlbumDetailSideEffect) -> Unit,
 ) {
+    if (state.importPhotoState.selectedPhotoIds.isEmpty()) {
+        postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 선택해주세요."))
+        return
+    }
     viewModelScope.launch {
         reduce { copy(importPhotoState = importPhotoState.copy(isLoading = true)) }
         folderRepository.copyPhotos(
             photoIds = state.importPhotoState.selectedPhotoIds.toList(),
             targetFolderIds = listOf(albumId),
📝 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
viewModelScope.launch {
reduce { copy(importPhotoState = importPhotoState.copy(isLoading = true)) }
folderRepository.copyPhotos(
photoIds = state.importPhotoState.selectedPhotoIds.toList(),
targetFolderIds = listOf(albumId),
).onSuccess {
reduce { copy(isShowImportPhotoBottomSheet = false, importPhotoState = ImportPhotoState()) }
postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 앨범에 추가했어요"))
postSideEffect(AlbumDetailSideEffect.PhotoImported(albumId))
if (state.importPhotoState.selectedPhotoIds.isEmpty()) {
postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 선택해주세요."))
return
}
viewModelScope.launch {
reduce { copy(importPhotoState = importPhotoState.copy(isLoading = true)) }
folderRepository.copyPhotos(
photoIds = state.importPhotoState.selectedPhotoIds.toList(),
targetFolderIds = listOf(albumId),
).onSuccess {
reduce { copy(isShowImportPhotoBottomSheet = false, importPhotoState = ImportPhotoState()) }
postSideEffect(AlbumDetailSideEffect.ShowToastMessage("사진을 앨범에 추가했어요"))
postSideEffect(AlbumDetailSideEffect.PhotoImported(albumId))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailViewModel.kt`
around lines 315 - 323, Before calling folderRepository.copyPhotos in the
viewModelScope.launch block, guard against an empty selection: check
state.importPhotoState.selectedPhotoIds (used in the current block) and if
empty, stop the flow (reset importPhotoState.isLoading = false if set, close the
bottom sheet or keep it open as desired) and optionally post a side effect like
a toast or no-op; only call folderRepository.copyPhotos when
selectedPhotoIds.isNotEmpty(). Update the logic around viewModelScope.launch /
reduce and the copyPhotos invocation to perform this early-return check so no
request is made with an empty list.

Comment on lines +71 to +73
val sheetHeight = remember {
configuration.screenHeightDp.dp - statusBarHeight - navigationBarHeight
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

sheetHeight 계산이 구성 변경 시 업데이트되지 않을 수 있습니다.

remember로 감싸진 sheetHeight는 화면 회전이나 inset 변경 시 재계산되지 않습니다. configuration과 inset 값을 key로 사용하거나 매번 계산하도록 수정하세요.

🐛 수정 제안
-    val sheetHeight = remember {
-        configuration.screenHeightDp.dp - statusBarHeight - navigationBarHeight
-    }
+    val sheetHeight = remember(configuration.screenHeightDp, statusBarHeight, navigationBarHeight) {
+        configuration.screenHeightDp.dp - statusBarHeight - navigationBarHeight
+    }
📝 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
val sheetHeight = remember {
configuration.screenHeightDp.dp - statusBarHeight - navigationBarHeight
}
val sheetHeight = remember(configuration.screenHeightDp, statusBarHeight, navigationBarHeight) {
configuration.screenHeightDp.dp - statusBarHeight - navigationBarHeight
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/ImportPhotoBottomSheet.kt`
around lines 71 - 73, The sheetHeight is stored with remember but not keyed to
changing inputs so it won't recompute on rotation or inset changes; update the
calculation of sheetHeight (the val sheetHeight using remember) to either
include configuration, statusBarHeight, and navigationBarHeight as remember keys
or remove remember and compute it directly each composition so that sheetHeight
recalculates when configuration or inset values change.

action: SelectAlbumAction,
title: String = "모든 앨범",
multiSelect: Boolean = true,
multiSelect: Boolean = false,
Copy link
Copy Markdown

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
# 목적: navigateToSelectAlbum(...) 호출 중 multiSelect 인자를 생략한 케이스 탐지
python - <<'PY'
from pathlib import Path

def extract_call(text, start_idx):
    i = start_idx
    depth = 0
    end = None
    while i < len(text):
        ch = text[i]
        if ch == '(':
            depth += 1
        elif ch == ')':
            depth -= 1
            if depth == 0:
                end = i
                break
        i += 1
    return end

for p in Path(".").rglob("*.kt"):
    try:
        s = p.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        continue
    idx = 0
    while True:
        idx = s.find("navigateToSelectAlbum(", idx)
        if idx == -1:
            break
        end = extract_call(s, idx + len("navigateToSelectAlbum"))
        if end is None:
            break
        call = s[idx:end+1]
        if "multiSelect" not in call:
            line = s.count("\n", 0, idx) + 1
            one_line = " ".join(call.split())
            print(f"{p}:{line}")
            print(f"  {one_line[:220]}")
        idx = end + 1
PY

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 733


🏁 Script executed:

# SelectAlbumNavKey.kt 파일 내용 확인
find . -name "SelectAlbumNavKey.kt" -type f

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 182


🏁 Script executed:

# 해당 파일의 내용 확인
cat -n $(find . -name "SelectAlbumNavKey.kt" -type f | head -1)

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 870


🏁 Script executed:

# 각 호출 위치에서의 context 확인
grep -B 2 -A 2 "navigateToSelectAlbum(action)" app/src/main/java/com/neki/android/app/MainActivity.kt | head -20

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 550


🏁 Script executed:

# 다른 호출 위치들도 확인
grep -B 2 -A 2 "navigateToSelectAlbum(sideEffect.action)" app/src/main/java/com/neki/android/app/main/MainScreen.kt

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 392


🏁 Script executed:

grep -B 2 -A 2 "navigateToSelectAlbum(sideEffect.photoIds)" feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 267


🏁 Script executed:

grep -B 2 -A 2 "navigateToSelectAlbum(sideEffect.photoId)" feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt

Repository: YAPP-Github/27th-App-Team-2-Android

Length of output: 341


기본값 변경으로 인한 5개 호출부 영향 확인 필요

multiSelect 기본값 변경이 5개 호출부에 영향을 미칩니다:

  • MainActivity.kt:126 - navigateToSelectAlbum(action)
  • MainScreen.kt:99 - navigateToSelectAlbum(sideEffect.action)
  • AlbumDetailScreen.kt:127 - navigateToSelectAlbum(sideEffect.action)
  • AllPhotoScreen.kt:126 - navigateToSelectAlbum(sideEffect.photoIds)
  • PhotoDetailScreen.kt:93 - navigateToSelectAlbum(sideEffect.photoId)

이들 호출부는 명시적으로 multiSelect 인자를 지정하지 않으므로 기본값(false)을 사용합니다. 기본값이 변경되었다면 이 5개 화면의 동작이 의도치 않게 변경되었을 가능성이 있습니다. 이 변경이 의도된 것인지 확인 후, 필요시 호출부를 업데이트하거나 기본값을 원래대로 복원하세요.

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

In
`@feature/select-album/api/src/main/java/com/neki/android/feature/select_album/api/SelectAlbumNavKey.kt`
at line 20, The default value for SelectAlbumNavKey.multiSelect was changed and
that affects callers that rely on the implicit default; update either the
default back to its original value or explicitly pass the desired boolean at
each call site: locate SelectAlbumNavKey (constructor/property multiSelect) and
the navigateToSelectAlbum usages (the functions invoking navigateToSelectAlbum
in MainActivity, MainScreen, AlbumDetailScreen, AllPhotoScreen,
PhotoDetailScreen) and ensure each call explicitly supplies
multiSelect=true/false to preserve intended behavior, or revert the multiSelect
default to its previous value to avoid changing behavior across those five
callers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant