Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
40fc91e
[feat]: 사진 이동/복제 core API 및 SelectAlbum 확장
ikseong00 Apr 11, 2026
81ccb7b
[feat] #180: PhotoDetail 앨범에 추가 기능
ikseong00 Apr 11, 2026
92bd9f7
[feat] #179: AllPhoto 다중선택 사진 복제 기능
ikseong00 Apr 11, 2026
a1257a7
[feat] #181: AlbumDetail 사진 복제/이동 기능
ikseong00 Apr 11, 2026
0c2ea95
[feat] #184: AlbumDetail 앨범 삭제 기능
ikseong00 Apr 11, 2026
155d322
[chore]: MovePhotosRequest / CopyPhotosRequest 모델 추가
ikseong00 Apr 11, 2026
d49d0a1
[chore]: detekt 린트 수정 및 토큰 로그 제거
ikseong00 Apr 11, 2026
640c59f
[chore]: 갤러리 아이콘 교체 및 복제 아이콘 추가
ikseong00 Apr 13, 2026
a616447
[chore]: NekiActionBar 컴포넌트 통일 및 ActionBar 분리
ikseong00 Apr 13, 2026
7688169
[feat]: 사진 복제/이동 API 및 SelectAlbum 액션 추가
ikseong00 Apr 13, 2026
7a6d3eb
[feat]: SelectAlbum 복제/이동 결과 이벤트 처리
ikseong00 Apr 13, 2026
fc69320
[feat]: 사진 복제/이동 완료 토스트 및 네비게이션 처리
ikseong00 Apr 13, 2026
aa924cb
[chore]: import 정리
ikseong00 Apr 13, 2026
c38cd1a
[feat]: 앨범 상세 사진 가져오기 바텀시트 구현
ikseong00 Apr 13, 2026
003b513
[feat]: 이동 시 출처 앨범 SelectAlbum에서 비활성화
ikseong00 Apr 13, 2026
f8de027
[feat]: 복제/이동 완료 토스트 중복 표시 방지 및 이동 토스트 추가
ikseong00 Apr 14, 2026
e347620
[refactor]: AlbumDetailViewModel 인텐트 핸들러 메서드 추출 및 코드 정리
ikseong00 Apr 14, 2026
afa5bd8
[feat] #182: 사진 가져오기 바텀시트 엠티뷰 추가 및 스크롤 끝 그라디언트 숨김
ikseong00 Apr 14, 2026
59ce5ea
[feat] #178: DropdownPopup 텍스트 색상 커스텀 기능 추가 및 앨범 삭제 메뉴 적용
ikseong00 Apr 14, 2026
65accbf
[chore] #178: PhotoDetail 앨범 추가 완료 토스트의 이동 버튼 제거 및 일반 토스트로 변경
ikseong00 Apr 14, 2026
2ad86e2
Merge remote-tracking branch 'origin/develop' into feat/4th-sprint
ikseong00 Apr 15, 2026
6ace4aa
[feat] #180: 사진 상세 앨범에 추가 후 액션토스트 앨범으로 버튼 연결
ikseong00 Apr 15, 2026
f4f3819
[chore]: 갤러리 추가 아이콘 리소스명 변경 및 적용
ikseong00 Apr 15, 2026
384b862
[chore]: PhotoDetailScreen 미사용 import 제거
ikseong00 Apr 15, 2026
e8ddfe1
[chore] #194: AlbumInfo color 파라미터를 isDisabled 패턴으로 변경
ikseong00 Apr 16, 2026
d67457f
[chore] #194: AllPhotoViewModel 불필요한 빈 사진 방어 로직 제거
ikseong00 Apr 16, 2026
f184330
[chore] #194: PhotoCopiedResult albumTitles를 단일 String으로 변경
ikseong00 Apr 16, 2026
4b2471f
[chore] #194: 중복 아이콘 리소스 제거 및 designsystem 모듈로 이동
ikseong00 Apr 16, 2026
2ece89f
[chore] #194: ImportPhotoBottomSheet 수동 높이 계산 제거 및 표준 방식 적용
ikseong00 Apr 16, 2026
dcade3e
[chore] #194: FavoriteAlbumActionBar 불필요한 contentPadding 제거
ikseong00 Apr 16, 2026
0741485
[chore]: AlbumDetailActionBar 리소스 임포트 및 별칭 정리
ikseong00 Apr 17, 2026
c8575ab
[fix] #194: 사진 추가 바텀시트에 페이지네이션 적용
ikseong00 Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions app/src/main/java/com/neki/android/app/main/MainScreen.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,39 @@
package com.neki.android.app.main

import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.ui.NavDisplay
import com.neki.android.app.main.component.AddPhotoBottomSheet
import com.neki.android.app.main.component.AlbumUploadOption
import com.neki.android.app.ui.BottomNavigationBar
import com.neki.android.app.main.component.SelectWithAlbumDialog
import com.neki.android.app.ui.BottomNavigationBar
import com.neki.android.core.navigation.result.LocalResultEventBus
import com.neki.android.core.navigation.result.ResultEffect
import com.neki.android.core.ui.component.LoadingDialog
import com.neki.android.core.ui.compose.collectWithLifecycle
import com.neki.android.core.ui.toast.NekiToast
import androidx.core.net.toUri
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import com.neki.android.feature.archive.api.ArchiveNavKey
import com.neki.android.feature.archive.api.PhotoUploadedResult
import com.neki.android.feature.map.api.MapNavKey
Expand All @@ -44,6 +42,8 @@ import com.neki.android.feature.photo_upload.api.PhotoUploadNavKey
import com.neki.android.feature.photo_upload.api.QRScanResult
import com.neki.android.feature.pose.api.PoseNavKey
import com.neki.android.feature.select_album.api.SelectAlbumAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import timber.log.Timber

@Composable
Expand Down Expand Up @@ -95,6 +95,7 @@ fun MainRoute(
MainSideEffect.OpenGallery -> photoPicker.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
)

is MainSideEffect.NavigateToSelectAlbum -> navigateToSelectAlbum(sideEffect.action)
is MainSideEffect.ShowToast -> nekiToast.showToast(sideEffect.message)
MainSideEffect.RefreshArchive -> resultBus.sendResult(result = PhotoUploadedResult, allowDuplicate = false)
Expand Down Expand Up @@ -127,7 +128,6 @@ fun MainScreen(
val shouldShowBottomBar by remember(currentKey) {
mutableStateOf(currentKey in topLevelKeys)
}

Scaffold(
modifier = Modifier
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private fun AddPhotoBottomSheetContent(
onClick = onClickQRScan,
)
AddPhotoOptionButton(
iconRes = R.drawable.icon_add_photo,
iconRes = R.drawable.icon_add_photo_gallery,
label = "갤러리에서 추가",
onClick = onClickGallery,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ interface FolderRepository {
suspend fun deleteFolder(id: List<Long>, deletePhotos: Boolean): Result<Unit>
suspend fun removePhotosFromFolder(folderId: Long, photoIds: List<Long>): Result<Unit>
suspend fun updateFolder(folderId: Long, name: String): Result<Unit>
suspend fun movePhotos(sourceFolderId: Long, photoIds: List<Long>, targetFolderIds: List<Long>): Result<Unit>
suspend fun copyPhotos(photoIds: List<Long>, targetFolderIds: List<Long>): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.neki.android.core.data.remote.api

import com.neki.android.core.data.remote.model.request.CopyPhotosRequest
import com.neki.android.core.data.remote.model.request.CreateFolderRequest
import com.neki.android.core.data.remote.model.request.DeleteFolderRequest
import com.neki.android.core.data.remote.model.request.DeletePhotoRequest
import com.neki.android.core.data.remote.model.request.MovePhotosRequest
import com.neki.android.core.data.remote.model.request.UpdateFolderRequest
import com.neki.android.core.data.remote.model.response.BasicNullableResponse
import com.neki.android.core.data.remote.model.response.BasicResponse
Expand Down Expand Up @@ -52,4 +54,14 @@ class FolderService @Inject constructor(
setBody(requestBody)
}.body()
}

// 사진 이동
suspend fun movePhotos(requestBody: MovePhotosRequest): BasicNullableResponse<Unit> {
return client.patch("/api/folders/photos/move") { setBody(requestBody) }.body()
}

// 사진 복제
suspend fun copyPhotos(requestBody: CopyPhotosRequest): BasicNullableResponse<Unit> {
return client.post("/api/folders/photos/copy") { setBody(requestBody) }.body()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.neki.android.core.data.remote.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CopyPhotosRequest(
@SerialName("photoIds") val photoIds: List<Long>,
@SerialName("targetFolderIds") val targetFolderIds: List<Long>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.neki.android.core.data.remote.model.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class MovePhotosRequest(
@SerialName("sourceFolderId") val sourceFolderId: Long,
@SerialName("photoIds") val photoIds: List<Long>,
@SerialName("targetFolderIds") val targetFolderIds: List<Long>,
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.neki.android.core.data.repository.impl

import com.neki.android.core.data.remote.api.FolderService
import com.neki.android.core.data.remote.model.request.CopyPhotosRequest
import com.neki.android.core.data.remote.model.request.CreateFolderRequest
import com.neki.android.core.data.remote.model.request.DeleteFolderRequest
import com.neki.android.core.data.remote.model.request.DeletePhotoRequest
import com.neki.android.core.data.remote.model.request.MovePhotosRequest
import com.neki.android.core.data.remote.model.request.UpdateFolderRequest
import com.neki.android.core.data.util.runSuspendCatching
import com.neki.android.core.dataapi.repository.FolderRepository
Expand Down Expand Up @@ -43,4 +45,16 @@ class FolderRepositoryImpl @Inject constructor(
requestBody = UpdateFolderRequest(name = name),
)
}

override suspend fun movePhotos(sourceFolderId: Long, photoIds: List<Long>, targetFolderIds: List<Long>): Result<Unit> = runSuspendCatching {
folderService.movePhotos(
requestBody = MovePhotosRequest(sourceFolderId = sourceFolderId, photoIds = photoIds, targetFolderIds = targetFolderIds),
)
}

override suspend fun copyPhotos(photoIds: List<Long>, targetFolderIds: List<Long>): Result<Unit> = runSuspendCatching {
folderService.copyPhotos(
requestBody = CopyPhotosRequest(photoIds = photoIds, targetFolderIds = targetFolderIds),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.neki.android.core.designsystem.actionbar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
Expand All @@ -21,9 +23,10 @@ import com.neki.android.core.designsystem.button.NekiIconButton
import com.neki.android.core.designsystem.ui.theme.NekiTheme

@Composable
fun NekiStartActionBar(
fun NekiActionBar(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
padding: PaddingValues = PaddingValues(),
content: @Composable RowScope.() -> Unit,
) {
Column(
modifier = modifier,
Expand All @@ -33,18 +36,22 @@ fun NekiStartActionBar(
thickness = 1.dp,
color = NekiTheme.colorScheme.gray75,
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart,
Row(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
}

@Composable
fun NekiEndActionBar(
fun NekiStartActionBar(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(),
content: @Composable () -> Unit,
) {
Column(
Expand All @@ -56,19 +63,21 @@ fun NekiEndActionBar(
color = NekiTheme.colorScheme.gray75,
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.fillMaxWidth()
.padding(padding),
contentAlignment = Alignment.CenterStart,
) {
content()
}
}
}

@Composable
fun NekiBothSidesActionBar(
fun NekiEndActionBar(
modifier: Modifier = Modifier,
startContent: @Composable () -> Unit,
endContent: @Composable () -> Unit,
padding: PaddingValues = PaddingValues(),
content: @Composable () -> Unit,
) {
Column(
modifier = modifier,
Expand All @@ -78,17 +87,49 @@ fun NekiBothSidesActionBar(
thickness = 1.dp,
color = NekiTheme.colorScheme.gray75,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
Box(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
contentAlignment = Alignment.CenterEnd,
) {
startContent()
endContent()
content()
}
}
}

@ComponentPreview
@Composable
private fun NekiActionBarPreview() {
NekiTheme {
NekiActionBar(
modifier = Modifier.fillMaxWidth(),
content = {
NekiIconButton(
modifier = Modifier.padding(8.dp),
onClick = {},
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_arrow_left),
contentDescription = null,
tint = NekiTheme.colorScheme.gray900,
)
}
NekiIconButton(
modifier = Modifier.padding(8.dp),
onClick = {},
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_bookmark_stroked),
contentDescription = null,
tint = NekiTheme.colorScheme.gray500,
)
}
},
)
}
}

@ComponentPreview
@Composable
private fun NekiStartActionBarPreview() {
Expand Down Expand Up @@ -131,37 +172,3 @@ private fun NekiEndActionBarPreview() {
}
}
}

@ComponentPreview
@Composable
private fun NekiBothSidesActionBarPreview() {
NekiTheme {
NekiBothSidesActionBar(
modifier = Modifier.fillMaxWidth(),
startContent = {
NekiIconButton(
modifier = Modifier.padding(8.dp),
onClick = {},
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_arrow_left),
contentDescription = null,
tint = NekiTheme.colorScheme.gray900,
)
}
},
endContent = {
NekiIconButton(
modifier = Modifier.padding(8.dp),
onClick = {},
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.icon_bookmark_stroked),
contentDescription = null,
tint = NekiTheme.colorScheme.gray500,
)
}
},
)
}
}
33 changes: 33 additions & 0 deletions core/designsystem/src/main/res/drawable/icon_copy_photo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<group>
<clip-path
android:pathData="M8.5,3L23.5,3A1.5,1.5 0,0 1,25 4.5L25,19.5A1.5,1.5 0,0 1,23.5 21L8.5,21A1.5,1.5 0,0 1,7 19.5L7,4.5A1.5,1.5 0,0 1,8.5 3z"/>
<path
android:pathData="M8.5,3L23.5,3A1.5,1.5 0,0 1,25 4.5L25,19.5A1.5,1.5 0,0 1,23.5 21L8.5,21A1.5,1.5 0,0 1,7 19.5L7,4.5A1.5,1.5 0,0 1,8.5 3z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#616575"/>
</group>
<path
android:pathData="M4,7V22.5C4,23.328 4.672,24 5.5,24H21"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#616575"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12H20"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#616575"
android:strokeLineCap="round"/>
<path
android:pathData="M16.006,8.006L16.006,16.006"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#616575"
android:strokeLineCap="round"/>
</vector>
Loading
Loading