Skip to content

[fix] #78 아카이브 업로드 QA 수정#84

Merged
Ojongseok merged 25 commits into
developfrom
fix/#78-archive-upload-qa
Feb 7, 2026
Merged

[fix] #78 아카이브 업로드 QA 수정#84
Ojongseok merged 25 commits into
developfrom
fix/#78-archive-upload-qa

Conversation

@ikseong00
Copy link
Copy Markdown
Contributor

@ikseong00 ikseong00 commented Feb 6, 2026

🔗 관련 이슈

📙 작업 설명

  • 이미지 업로드 시 EXIF 회전 방향 보정 적용
  • 앨범 커버 블러 효과(Haze) 적용
  • Empty 뷰 디자인 수정 및 EmptyContent 공통화
  • 포토 그리드 아이템 오버레이 추가
  • ToolTipPopup 공통 컴포저블 추가 및 dismiss 분리
  • 로딩 인디케이터 Lottie 애니메이션으로 변경
  • 네키 타이포 로고 아이콘 추가
  • PhotoComponent cornerRadius 8dp로 수정
  • SelectablePhotoItem 선택 시 검은 배경 오버레이 제거
  • AlbumDetailContent 접근제어자 internal로 변경
  • backgroundHazeBlur 미사용 파라미터 제거 및 KDoc 업데이트
  • 즐겨찾는 사진 앨범명 "즐겨찾기"로 수정

📷 스크린샷

Summary by CodeRabbit

  • New Features

    • 네키 타이포 로고 컴포넌트(테마별)
    • 툴팁 팝업 컴포넌트 추가
    • Lottie 기반 로딩 애니메이션 도입
    • 빈 상태 일러스트 및 빈 화면 콘텐츠 개선
    • 이미지 업로드 시 EXIF 방향 자동 보정 지원
  • Style

    • 레이블 "즐겨찾는 사진" → "즐겨찾기"로 변경
    • 사진 그리드 모서리 반경 12dp → 8dp
    • 타이포·간격·배경 흐림 시각 조정
  • Refactor

    • 사진 선택·오버레이 렌더링 구조 정리
    • 화면별 로딩/빈 상태 처리 재구성

앨범 전체 보기 화면의 `LazyColumn`에서 `verticalArrangement` 속성을 제거하여 아이템 간의 간격을 조정했습니다.
Lottie 애니메이션을 표시하기 위해 `lottie-compose` 라이브러리 종속성을 추가하고, `loading_animation.json` 파일을 리소스로 추가했습니다.
기존 `CircularProgressIndicator`를 사용하던 로딩 인디케이터를 Lottie 애니메이션으로 교체했습니다.
네키 타이포 로고(Neki Typo Logo) 벡터 아이콘과 이를 사용하는 컴포저블 함수(`PrimaryNekiTypoLogo`, `GrayNekiTypoLogo`, `WhiteNekiTypoLogo`)를 추가했습니다.
아카이브 화면의 앨범 커버 디자인을 개선하고, `dev.chrisbanes.haze` 라이브러리를 사용해 블러 효과를 적용했습니다.

- 앨범 커버를 `drawBehind` 대신 `Shape`으로 구현하여 재사용성을 높이고, `clip`을 사용하도록 변경
- `backgroundHazeBlur` Modifier 확장 함수를 추가하여 블러 효과를 공통으로 처리
- `AlbumFolderLayout`에 `Haze`를 적용하고, `RandomPoseTutorialOverlay`에도 이를 재사용하여 적용
- 앨범 커버 내 아이콘, 텍스트 스타일 및 레이아웃 등 전반적인 UI 디테일 수정
- `PhotoTitleRow` 파일명을 `ArchiveMainTitleRow`로 변경하여 명확성 개선
기존 `ArchiveMainTopBar` 내부에만 있던 툴팁 로직을, 디자인시스템의 범용 `ToolTipPopup` 컴포저블로 분리하고 이를 적용했습니다.
기존 `PhotoComponent`에 종속되어 있던 그라데이션 및 선택 효과 로직을 `PhotoGridItemOverlay`와 `SelectedPhotoGridItemOverlay` 컴포저블로 분리했습니다. 이를 통해 사진 그리드 아이템의 UI를 보다 독립적으로 관리하고 재사용성을 높였습니다.

- `PhotoGridItemOverlay`, `SelectedPhotoGridItemOverlay` 컴포저블 추가
- `photoBackground` Modifier를 `photoGridBackground`로 변경하고, 그라데이션 효과를 상단에서 시작하도록 수정
- `PhotoComponent`에서 불필요한 `additionalContent` 파라미터 제거
- 아카이브 화면의 `SelectablePhotoItem`과 `ArchiveMainPhotoItem`에 새로운 오버레이 컴포저블 적용
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 6, 2026

Walkthrough

로고 컴포넌트·벡터 리소스 추가, Lottie 로딩 도입, backgroundHazeBlur API 변경, 툴팁 팝업·빈 상태 벡터 추가, EXIF 회전 처리, 아카이브·업로드 UI 리팩토링 및 문자열·빌드 설정 업데이트가 포함됩니다.

Changes

Cohort / File(s) Summary
Logo & Drawable
core/designsystem/src/main/java/com/neki/android/core/designsystem/logo/NekiTypoLogo.kt, core/designsystem/src/main/res/drawable/icon_neki_logo_typo.xml
Neki Typo 로고 컴포저블(Primary/Gray/White) 추가 및 벡터 drawable 추가.
Loading / Lottie
core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt, core/ui/src/main/res/raw/loading_animation.json, core/ui/build.gradle.kts
CircularProgressIndicator → Lottie 애니메이션으로 대체, size 파라미터 추가, lottie 의존성 및 애니메이션 리소스 추가.
Background Modifier
core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt
backgroundHazeBlur에서 @Composable 제거 및 파라미터 변경(명시적 alpha, color, blurRadius 등), tint 계산을 color.copy(alpha=...)로 변경.
Tooltip Popup
core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt
새 ToolTipPopup 컴포저블 추가(팝업 앵커·꼬리 캔버스·본문 텍스트).
Empty State UI & Asset
core/designsystem/src/main/res/drawable/icon_empty_content.xml, feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt
빈 상태 벡터 리소스 추가; EmptyContent 시그니처 변경(title, emptyText, onClickBack) 및 상단바 포함 레이아웃 변경.
Album Detail & TopBar
feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AlbumDetailScreen.kt, feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt
AlbumDetailContent 분리·도입, derivedState 기반 로딩/빈 상태 처리, SelectMode 기반 TopBar 컴포넌트 추가.
Archive Main & ToolTip Integration
feature/archive/impl/src/main/kotlin/.../main/component/ArchiveMainTopBar.kt, feature/archive/impl/src/main/kotlin/.../main/ArchiveMainScreen.kt, feature/archive/impl/src/main/kotlin/.../main/component/ArchiveMainAlbumList.kt, feature/archive/impl/src/main/kotlin/.../main/component/ArchiveMainPhotoItem.kt, feature/archive/impl/src/main/kotlin/.../main/component/ArchiveMainTitleRow.kt
PrimaryNekiTypoLogo 적용, 툴팁 캔버스 → ToolTipPopup 전환, 팝업 콜백 분리(onDismissAddPopup/onDismissToolTipPopup), AlbumFolderShape 및 haze 적용, UI·타이포 변경.
Selectable / Item Overlays
feature/archive/impl/src/main/kotlin/.../component/SelectablePhotoItem.kt, core/ui/src/main/java/com/neki/android/core/ui/component/ItemOverlay.kt, core/ui/src/main/java/com/neki/android/core/ui/component/PhotoComponent.kt
선택 UI를 오버레이(SelectedPhotoGridItemOverlay/PhotoGridItemOverlay) 기반으로 리팩토링; PhotoComponent에서 additionalContent 제거 및 코너 반경 12dp→8dp 조정.
Album / Photo Screens
feature/archive/impl/src/main/kotlin/.../album/AllAlbumScreen.kt, feature/archive/impl/src/main/kotlin/.../photo/AllPhotoScreen.kt
LazyColumn 간격 제거, AllPhotoContent/AlbumDetailContent 분리·구조화, 빈 상태 및 로딩/삭제 다이얼로그 상위로 이동, 문자열("즐겨찾기") 일관화.
Upload Feature & BuildConfig
feature/photo-upload/impl/src/main/kotlin/.../UploadAlbumScreen.kt, feature/photo-upload/impl/build.gradle.kts, feature/photo-upload/impl/src/main/java/.../qrscan/PhotoWebViewClient.kt
LazyColumn 간격 제거, BuildConfig 필드 추가·명명 정리(PHOTOISM_IMAGE_URL 등), WebView MIME 상수 참조 업데이트.
EXIF Orientation & Utilities
core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt, core/common/build.gradle.kts, gradle/libs.versions.toml
Uri.toByteArray에 EXIF 회전/플립 적용 로직 추가, ExifInterface 의존성 추가 및 libs 버전 항목 추가(lottie, exifinterface).
Minor Text/comment changes
core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt, core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt, 여러 preview 파일
주석·레이블 일부 변경("즐겨찾는 사진" → "즐겨찾기") 및 프리뷰 텍스트 조정.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Ojongseok

Poem

🐰 새 로고 폰트 반짝여요
팝업 말풍선 살며시 떠요
로딩은 춤추며 반짝이고
사진은 방향 맞춰 빙글돌고
즐겨찾기 화면에 봄이 왔네 ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR은 컴포넌트 사이즈 수정, UI/UX 개선 등을 구현했으나 이슈 #78의 QR 관련 수정과 카메라 권한 수정은 구현되지 않았습니다. 이슈 #78에서 요구한 QR 관련 수정과 카메라 권한 수정을 확인하고 필요시 별도 작업으로 진행하거나 이슈 범위를 명확히 조정하세요.
Out of Scope Changes check ⚠️ Warning 일부 변경사항이 이슈 범위를 벗어났습니다. 특히 Lottie 애니메이션 로딩 인디케이터, 네키 타이포 로고, 즐겨찾기 문자열 수정 등은 QR/카메라 권한과 무관하며 추가적인 UI 개선 작업입니다. 이슈 #78의 요구사항과 직접 관련 없는 변경사항들을 명확히 문서화하거나 별도의 개선 사항으로 분류하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 '#78 아카이브 업로드 QA 수정'으로 변경사항의 주요 목표인 아카이브/업로드 QA 항목 반영을 명확히 설명하고 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#78-archive-upload-qa

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 self-assigned this Feb 6, 2026
@ikseong00 ikseong00 added the feat label Feb 6, 2026
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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt (1)

57-68: ⚠️ Potential issue | 🟡 Minor

주석과 함수 시그니처 간 불일치

주석이 "즐겨찾는 앨범 조회"로 변경되었으나, 함수명은 getFavoritePhotos, 반환 타입은 FavoritePhotoResponse, 엔드포인트는 /api/photos/favorite로 모두 "사진(Photo)" 기준입니다. 주석이 실제 동작과 맞지 않으면 혼동을 유발할 수 있으니, 주석 또는 함수명 중 하나를 통일하는 것이 좋겠습니다.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt (1)

1-1: ⚠️ Potential issue | 🟡 Minor

파일명 오타: "Selcetable" → "Selectable"

파일명이 SelcetablePhotoItem.kt으로 되어 있지만, 함수명은 올바르게 SelectablePhotoItem입니다. 파일명을 SelectablePhotoItem.kt으로 수정하면 일관성이 높아집니다.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt (1)

68-78: ⚠️ Potential issue | 🟡 Minor

showAddPopupshowTooltip이 동시에 true일 경우 팝업이 겹칠 수 있습니다.

AddPhotoPopupArchiveToolTip의 offset이 유사한 위치(각각 48dp, 47dp)이므로, 두 팝업이 동시에 표시되면 시각적으로 겹칩니다. 호출부에서 상태를 배타적으로 관리하거나, 여기서 가드를 추가하는 것을 권장합니다.

🔧 가드 조건 추가 제안
                     if (showAddPopup) {
                         AddPhotoPopup(
                             onDismissRequest = onDismissPopup,
                             onClickQRScan = onClickQRScan,
                             onClickGallery = onClickGallery,
                             onClickNewAlbum = onClickNewAlbum,
                         )
                     }
-                    if (showTooltip) {
+                    if (showTooltip && !showAddPopup) {
                         ArchiveToolTip()
                     }
🤖 Fix all issues with AI agents
In
`@core/designsystem/src/main/java/com/neki/android/core/designsystem/logo/NekiTypoLogo.kt`:
- Around line 73-79: WhiteNekiTypoLogoPreview uses `@Preview` and renders a white
logo on the theme's white background so it's invisible; update the preview to
use the project's `@ComponentPreview` (for consistency) or change the preview to
render on a dark background (e.g., wrap WhiteNekiTypoLogo in a container/Surface
with a dark color or call NekiTheme with darkTheme=true) so the
WhiteNekiTypoLogo is visible; modify the WhiteNekiTypoLogoPreview function (and
its annotation) and ensure it still composes NekiTheme and calls
WhiteNekiTypoLogo.

In
`@core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt`:
- Around line 41-62: The Modifier.backgroundHazeBlur implementation ignores the
shape parameter when enabled is true, causing visual mismatch; update the
enabled=true branch to pass the shape into the hazeEffect (or otherwise apply
the provided Shape) by adding shape to the hazeEffect / HazeStyle call
(reference backgroundHazeBlur, hazeEffect, HazeStyle), and add KDoc `@param`
entries describing alpha and shape so both parameters are documented.

In
`@core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt`:
- Around line 25-41: The Popup currently cannot be dismissed by outside touch;
update ToolTipPopup to accept an onDismissRequest: () -> Unit parameter and pass
it to Popup as onDismissRequest, and set PopupProperties(focusable = true) (and
any other desired properties like dismissOnBackPress if needed) so outside taps
will dismiss; keep invoking ToolTipContent(tooltipText, color) as before. Use
the ToolTipPopup function name and Popup/PopupProperties symbols to locate where
to add the new parameter and attributes.

In `@core/designsystem/src/main/res/drawable/icon_empty_content.xml`:
- Around line 10-12: 아이콘 배경으로 하드코딩된 불투명 흰색 사각형(<path
android:pathData="M0,0h172v112h-172z" android:fillColor="#ffffff"/>)이 포함되어 있어
비흰색 배경에 배치 시 흰색 박스가 나타납니다; 이 <path> 요소를 삭제하거나 android:fillColor를 투명
값("#00000000")으로 변경하고, 아이콘이 투명 배경에서 의도대로 보이는지 (다른 패스들과 겹침/정렬 등) 확인하세요.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt`:
- Around line 152-159: AlbumDetailContent is declared public while
AlbumDetailScreen and AlbumDetailRoute are internal; make AlbumDetailContent
internal to unify visibility by changing its declaration from "fun
AlbumDetailContent(...)" (public) to "internal fun AlbumDetailContent(...)" and
update any internal callers if necessary so the function's visibility matches
AlbumDetailScreen and AlbumDetailRoute.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt`:
- Around line 58-68: The overlays use RoundedCornerShape(8.dp) which mismatches
PhotoComponent's RoundedCornerShape(12.dp) (and the selection border), causing
visual mismatch; update SelectedPhotoGridItemOverlay and PhotoGridItemOverlay
invocations in SelcetablePhotoItem (the block that checks isSelected) to use
RoundedCornerShape(12.dp) so the overlay corners align with PhotoComponent's
clipping and selection border.
- Around line 37-68: The selected state currently applies the same
semi-transparent black twice (once in PhotoComponent's modifier background and
again via SelectedPhotoGridItemOverlay); remove the background from
PhotoComponent when isSelected and only keep the darker visual from
SelectedPhotoGridItemOverlay so the dimming isn't doubled. Concretely, update
the conditional in the PhotoComponent modifier (around the isSelected branch) to
apply .border(...) and .clip(...) when isSelected but omit .background(...);
leave SelectedPhotoGridItemOverlay as-is to provide the selected dim overlay.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainPhotoItem.kt`:
- Around line 27-38: The overlay and photo clipping radii differ: PhotoComponent
clips its content with RoundedCornerShape(12.dp) while PhotoGridItemOverlay is
given RoundedCornerShape(8.dp), causing visual mismatch; fix by making them
consistent—either change the shape passed into PhotoGridItemOverlay to
RoundedCornerShape(12.dp) or apply the same clip(RoundedCornerShape(12.dp)) to
the outer Box that wraps PhotoComponent and PhotoGridItemOverlay so both use the
identical 12.dp radius (refer to PhotoComponent, PhotoGridItemOverlay and the
surrounding Box in ArchiveMainPhotoItem).

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt`:
- Around line 94-107: The tooltip Popup cannot be dismissed by external touch or
back button because ToolTipPopup is used without onDismissRequest/focusable
handling; update either ToolTipPopup to accept and pass an onDismissRequest
lambda and set focusable = true on its Popup (so external taps/back press call
the callback), or implement a timed dismiss inside ArchiveToolTip that calls a
provided dismiss callback after N milliseconds; refer to ToolTipPopup,
ArchiveToolTip and AddPhotoPopup for the pattern (use onDismissRequest and
focusable = true as in AddPhotoPopup) and wire an onDismissRequest up to the
caller so the tooltip can be dismissed.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoEmptyContent.kt`:
- Around line 24-66: AllPhotoEmptyContent is dead/duplicated UI (duplicate of
EmptyContent) and its AllPhotoTopBar has no-op handlers; remove the
AllPhotoEmptyContent composable and any associated preview functions, update any
local references to use EmptyContent instead (e.g., where AllPhotoEmptyContent
was referenced in previews or tests), and delete unused imports; also remove the
unused empty lambda handlers tied to AllPhotoTopBar within the removed code.
🧹 Nitpick comments (10)
feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt (4)

100-105: derivedStateOf 사용이 적절하나, isEmpty 판정 시 초기 로딩 상태 고려 필요.

pagingItems.loadState.refresh is LoadState.NotLoading 조건으로 초기 로딩 중 빈 화면이 잠깐 보이는 문제를 방지한 점이 좋습니다.

다만, remember 블록이 키 없이 사용되고 있어 pagingItems 객체가 교체되는 경우(예: Configuration change 이외의 상위 recomposition으로 새 LazyPagingItems 인스턴스가 전달되는 경우)에는 stale 참조가 남을 수 있습니다. 현재 구조에서는 collectAsLazyPagingItems()가 내부적으로 snapshot state를 사용하므로 문제가 없을 것으로 보이지만, 방어적으로 키를 추가하는 것을 고려해 볼 수 있습니다.

♻️ 방어적 키 추가 제안
-    val isRefreshing by remember {
+    val isRefreshing by remember(pagingItems) {
         derivedStateOf { pagingItems.loadState.refresh is LoadState.Loading }
     }
-    val isEmpty by remember {
+    val isEmpty by remember(pagingItems) {
         derivedStateOf { pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading }
     }

111-115: 타이틀 로직이 두 곳에서 중복됩니다.

if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title 로직이 Line 113 (EmptyContent)과 Line 166 (AlbumDetailTopBar) 양쪽에 동일하게 존재합니다. 향후 타이틀 규칙 변경 시 한 쪽만 수정하는 실수를 방지하기 위해, AlbumDetailScreen 레벨에서 한 번만 계산하여 내려주는 것을 권장합니다.

♻️ 타이틀 로직 통합 제안

AlbumDetailScreen 내에서 타이틀을 미리 계산:

+    val displayTitle = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title
+
     if (isEmpty) {
         EmptyContent(
-            title = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title,
+            title = displayTitle,
             onClickBack = { onIntent(AlbumDetailIntent.ClickBackIcon) },
         )
     } else {

그리고 AlbumDetailContenttitle 파라미터를 추가하거나, 내부에서도 동일하게 전달된 값을 사용하도록 수정합니다.

Also applies to: 165-166


125-127: LoadingDialog가 dismiss 불가능한 상태로 표시됩니다.

LoadingDialog()가 기본 파라미터로 호출되고 있으며, 외부 컨텍스트에서 onDismissRequest = {}가 기본값입니다. isRefreshing이 네트워크 지연 등으로 오래 유지될 경우 사용자가 dismiss할 수 없어 UI가 블로킹됩니다. 의도된 동작이라면 무시하셔도 됩니다.


234-257: 프리뷰에서 @Preview 대신 @ComponentPreview 사용을 고려해 주세요.

AlbumDetailTopBar.kt에서는 @ComponentPreview를 사용하고 있지만, 이 파일에서는 @Preview를 직접 사용하고 있습니다. 프로젝트 내 프리뷰 어노테이션 사용이 일관되지 않습니다.

core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt (1)

43-50: album.title을 무시하고 하드코딩된 문자열 사용

FavoriteAlbumRowComponentAlbumPreview 객체를 받지만, album.title을 사용하지 않고 "즐겨찾기"를 하드코딩하고 있습니다. 이렇게 하면 호출부에서 전달한 title 값이 무시되어 혼동을 줄 수 있습니다.

의도적으로 고정 타이틀을 사용하는 경우라면, 파라미터에서 AlbumPreview 대신 필요한 필드(thumbnailUrl, photoCount)만 받거나, 명시적으로 상수를 사용하는 방식을 고려해 보세요.

♻️ 제안: 필요한 필드만 파라미터로 받기
 `@Composable`
 fun FavoriteAlbumRowComponent(
-    album: AlbumPreview,
+    thumbnailUrl: String?,
+    photoCount: Int,
     modifier: Modifier = Modifier,
     onClick: () -> Unit = {},
 ) {
     Row(
         modifier = modifier
             .noRippleClickable(onClick = onClick)
             .fillMaxWidth()
             .padding(vertical = 10.dp, horizontal = 20.dp),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(16.dp),
     ) {
         FavoriteAlbumThumbnail(
-            thumbnailUrl = album.thumbnailUrl,
+            thumbnailUrl = thumbnailUrl,
         )
 
         AlbumInfo(
             title = "즐겨찾기",
-            photoCount = album.photoCount,
+            photoCount = photoCount,
         )
     }
 }
core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt (1)

52-83: 꼬리(tail) 위치가 오른쪽 정렬로 하드코딩되어 있습니다.

현재 꼬리가 padding(end = 16.dp) + Alignment.CenterEnd로 항상 오른쪽에 고정됩니다. 공통 컴포넌트로서 다양한 방향에서 사용하려면, 꼬리 정렬을 파라미터로 받는 것을 고려해 보세요.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainAlbumList.kt (1)

196-208: AlbumFolderhazeState 기본값이 블러 소스 없이 생성됩니다.

AlbumFolderprivate 함수이고, 실제로는 ArchiveAlbumItem에서 항상 명시적으로 hazeState를 전달하므로 현재 문제는 없습니다. 다만 rememberHazeState() 기본값은 소스가 연결되지 않은 상태라 블러가 동작하지 않으므로, 혼동 방지를 위해 기본값을 제거하는 것도 고려해 볼 수 있습니다. AlbumFolderLayout (Line 266)에도 동일하게 적용됩니다.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt (1)

59-70: EmptyTopBarBackTitleTopBar의 단순 래퍼입니다.

현재 추가 로직 없이 파라미터를 전달만 하고 있으므로, EmptyContent 내에서 BackTitleTopBar를 직접 호출해도 무방합니다. 다만 private 함수이고 향후 확장 가능성을 고려하면 현재 상태도 수용 가능합니다.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt (1)

260-266: 빈 프리뷰 함수는 제거하거나 구체화하는 것이 좋습니다.

현재 AllPhotoScreenPreview는 주석만 포함되어 있고 실제 UI를 렌더링하지 않습니다. Paging을 프리뷰에서 사용하기 어려운 점은 이해하지만, 빈 프리뷰는 유지 보수 가치가 낮으므로 제거하거나, flowOf(PagingData.from(sampleList)).collectAsLazyPagingItems()와 같은 패턴으로 간단한 샘플 데이터를 제공하는 것을 고려해 보세요.

core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt (1)

20-35: LoadingDialog에서 modifier.size(size)로 전달 시 .size()가 중복 적용됩니다.

LoadingDialogmodifier.size(200.dp)LoadingIndicator에 전달하면, LoadingIndicator 내부에서 다시 .size(80.dp)가 체이닝됩니다. Compose에서는 첫 번째 .size()가 우선하므로 현재는 200dp로 렌더링되어 동작상 문제는 없지만, 의도가 불명확하고 유지보수 시 혼동을 줄 수 있습니다.

LoadingDialog에서는 size 값을 LoadingIndicatorsize 파라미터로 전달하는 것이 명확합니다.

♻️ 수정 제안
 fun LoadingDialog(
     modifier: Modifier = Modifier,
     size: Dp = 200.dp,
     properties: DialogProperties = DialogProperties(),
     onDismissRequest: () -> Unit = {},
 ) {
     Dialog(
         onDismissRequest = onDismissRequest,
         properties = properties,
     ) {
         LoadingIndicator(
-            modifier = modifier.size(size),
+            modifier = modifier,
+            size = size,
         )
     }
 }

Comment thread core/designsystem/src/main/res/drawable/icon_empty_content.xml
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: 2

🤖 Fix all issues with AI agents
In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt`:
- Around line 237-244: The preview date string for val photos in
AlbumDetailScreen (the Photo objects created in the photos map) can become
invalid for it >= 10 (e.g., "2024-04-210"); change the date generation to
produce a valid day component (for example compute an integer day and
interpolate it) such as using "2024-04-${20 + it}" or formatting a zero-padded
day with String.format("%02d", day) so all Photo.date values are valid ISO-like
dates and won't crash date-parsing logic.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt`:
- Line 1: Rename the file from SelcetablePhotoItem.kt to SelectablePhotoItem.kt
so the filename matches the existing symbol SelectablePhotoItem; update any
references/imports that still point to the old filename (e.g., import
statements, module registrations or tests) to use SelectablePhotoItem, and
ensure your VCS commit preserves the rename (git mv) to avoid duplicate class
files.
🧹 Nitpick comments (7)
core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt (1)

26-34: 그라데이션 stop에 매직 넘버 134f / 242f 사용.

134f / 242f는 특정 디자인 스펙(컴포넌트 높이 242px 중 134px 지점)에서 유래한 것으로 보입니다. 컴포넌트 높이가 변경되면 의도와 달라질 수 있으므로, 의미를 명확히 하는 상수나 주석을 추가하면 유지보수에 도움이 됩니다.

예시
+// 디자인 스펙: 242px 높이 기준 134px 지점에서 그라데이션 종료
+private const val GRADIENT_END_FRACTION = 134f / 242f
+
 fun Modifier.photoGridBackground(
     shape: Shape = RoundedCornerShape(8.dp),
 ): Modifier = this
     .background(
         color = Color.Black.copy(alpha = 0.04f),
         shape = shape,
     )
     .background(
         brush = Brush.verticalGradient(
             colorStops = arrayOf(
                 0f to Color.Black.copy(alpha = 0.2f),
-                134f / 242f to Color.Black.copy(alpha = 0f),
+                GRADIENT_END_FRACTION to Color.Black.copy(alpha = 0f),
             ),
         ),
         shape = shape,
     )
core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt (1)

21-36: 대용량 이미지에서 OOM 위험이 있습니다.

현재 전체 바이트 배열(bytes) + 디코딩된 비트맵(bitmap) + 회전된 비트맵(rotatedBitmap) + 출력 스트림이 동시에 메모리에 존재합니다. 고해상도 이미지의 경우 약 3~4배의 메모리를 소비하여 OOM이 발생할 수 있습니다.

BitmapFactory.OptionsinSampleSize를 활용하거나 최대 해상도를 제한하는 방식을 고려해 보세요.

♻️ 예시: inSampleSize를 활용한 메모리 최적화
+private const val MAX_DIMENSION = 4096
+
 fun Uri.toByteArray(
     context: Context,
     quality: Int = DEFAULT_QUALITY,
     format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
 ): ByteArray? {
     val orientation = context.contentResolver.openInputStream(this)?.use { input ->
         ExifInterface(input).getAttributeInt(
             ExifInterface.TAG_ORIENTATION,
             ExifInterface.ORIENTATION_UNDEFINED,
         )
     } ?: ExifInterface.ORIENTATION_UNDEFINED
     val bytes = context.contentResolver.openInputStream(this)?.use { it.readBytes() } ?: return null
-    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return null
+    val options = BitmapFactory.Options().apply {
+        inJustDecodeBounds = true
+        BitmapFactory.decodeByteArray(bytes, 0, bytes.size, this)
+        inSampleSize = calculateInSampleSize(this, MAX_DIMENSION, MAX_DIMENSION)
+        inJustDecodeBounds = false
+    }
+    val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) ?: return null
     val rotatedBitmap = bitmap.applyOrientation(orientation)
feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt (2)

100-105: rememberpagingItems 키를 추가하는 것을 권장합니다.

derivedStateOf 클로저가 pagingItems 참조를 캡처하고 있지만, remember에 키가 없으므로 pagingItems 인스턴스가 변경되면(예: 구성 변경 또는 navigation 재진입 시) 이전 인스턴스를 계속 참조하게 됩니다.

실제로 collectAsLazyPagingItems()가 안정적인 참조를 반환하는 경우가 많지만, 안전한 패턴을 위해 키를 명시하는 것이 좋습니다.

♻️ 수정 제안
-    val isRefreshing by remember {
+    val isRefreshing by remember(pagingItems) {
         derivedStateOf { pagingItems.loadState.refresh is LoadState.Loading }
     }
-    val isEmpty by remember {
+    val isEmpty by remember(pagingItems) {
         derivedStateOf { pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading }
     }

111-123: 타이틀 로직이 중복되어 있습니다.

Line 113과 Line 166에서 if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title 동일 로직이 반복됩니다. 한쪽만 수정 시 불일치가 발생할 수 있으므로, 상위에서 한 번만 계산하여 전달하거나 AlbumDetailState의 프로퍼티로 추출하는 것을 권장합니다.

♻️ 예시: AlbumDetailScreen에서 로컬 변수로 추출
+    val displayTitle = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title
+
     if (isEmpty) {
         EmptyContent(
-            title = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title,
+            title = displayTitle,
             onClickBack = { onIntent(AlbumDetailIntent.ClickBackIcon) },
         )
     } else {

그리고 AlbumDetailContent에도 displayTitle을 파라미터로 전달하거나, 동일하게 적용합니다.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainContract.kt (1)

18-18: isShowAddDialog 네이밍이 리네이밍된 intent와 불일치합니다.

Intent가 DismissAddDialogDismissAddPopup으로 변경되었지만, 상태 필드는 여전히 isShowAddDialog로 남아 있습니다. 일관성을 위해 isShowAddPopup으로 변경하는 것을 고려해 주세요.

feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt (2)

47-47: isFirstEntered = false 설정이 중복됩니다.

Line 47에서 EnterArchiveMainScreen을 제외한 모든 intent에 대해 isFirstEntered = false를 설정하고 있어, Line 58의 ClickScreen 핸들러와 Line 64의 DismissToolTipPopup 핸들러에서 동일한 reduce가 불필요하게 반복됩니다.

의도가 "어떤 인터랙션이든 툴팁을 닫는다"라면 Line 47의 blanket 처리로 충분하며, DismissToolTipPopup intent 자체가 필요 없을 수 있습니다. 반대로 특정 액션에서만 툴팁을 닫으려는 의도라면 Line 47의 blanket 처리를 제거하고 개별 핸들러에서 명시적으로 관리하는 것이 더 명확합니다.

현재 구조에서는 DismissToolTipPopup의 역할이 모호합니다.

Also applies to: 58-58, 63-64


122-137: fetchInitialData의 에러 처리 확인.

awaitAll 내부의 개별 fetch 함수(fetchFavoriteSummary, fetchPhotos, fetchFolders)가 각각 onFailure에서 로깅만 수행하므로, 하나가 실패해도 다른 요청은 정상 진행되고 isLoading = false로 전환됩니다. 현재 구조상 사용자에게 에러 피드백이 전달되지 않는 점은 인지하고 계신지 확인 부탁드립니다.

`offset`으로 구현되어 있던 버튼의 패딩을 `contentPadding`을 사용하도록 변경하여 터치 영역을 확장했습니다.
`build.gradle.kts` 파일에 정의된 일부 BuildConfig 필드명의 오타를 수정합니다.

- `PHOTOISM_IMG_URL_MIME_TYPE` → `PHOTOISM_IMAGE_URL_MIME_TYPE`
- `LIFE_FOUR_CUT_URL_MIME_TYPE` → `LIFE_FOUR_CUT_IMAGE_URL_MIME_TYPE`

수정된 필드명을 참조하도록 `PhotoWebViewClient.kt` 파일도 함께 변경했습니다.
Copy link
Copy Markdown
Member

@Ojongseok Ojongseok left a comment

Choose a reason for hiding this comment

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

Q. 이미지 백그라운드 설정을 Overlay 형식으로 했습니다. 이 부분 괜찮을까요?

Modifier.xxxBackground()가 아닌 별도로 컴포저블 컴포넌트 분리하여 사용하는 해당 PR에서의 형식 말씀하시는게 맞을까요? -> 맞다면 저는 해당 방식처럼 :core:ui나 :feature:component에 별도 컴포넌트로 사용하는게 좀 더 편한 것 같습니다.

`AlbumDetailTopBar` 컴포저블의 `onClickBack`, `onClickSelect`, `onClickCancel` 파라미터에 기본으로 빈 람다 `{}`를 할당했습니다. 이를 통해 호출부에서 불필요한 빈 콜백을 매번 전달해야 하는 코드를 간소화했습니다.
`SelcetablePhotoItem`의 파일명을 `SelectablePhotoItem`으로 수정하고, 선택된 사진 아이템의 테두리 radius 값을 12dp에서 8dp로 변경했습니다.
`photoGridBackground` Modifier 확장 함수를 제거하고, `ItemOverlay` 컴포저블 내부에서 `Modifier.background`를 직접 사용하도록 수정했습니다.
@ikseong00
Copy link
Copy Markdown
Contributor Author

Modifier.xxxBackground()가 아닌 별도로 컴포저블 컴포넌트 분리하여 사용하는 해당 PR에서의 형식 말씀하시는게 맞을까요? -> 맞다면 저는 해당 방식처럼 :core:ui나 :feature:component에 별도 컴포넌트로 사용하는게 좀 더 편한 것 같습니다.

기존 Modifier.xxxBackground() 확장함수를 오버레이에서만 사용하고 있어서 확장함수를 제거하고 Overlay 컴포넌트로 유지해도 괜찮을 것 같습니다!
b233af2

포즈피드 랜덤 아이템 UI에 그라데이션 배경과 흐림(blur) 효과를 적용했습니다.
@Ojongseok Ojongseok merged commit 1f70ce3 into develop Feb 7, 2026
2 checks passed
@ikseong00 ikseong00 deleted the fix/#78-archive-upload-qa branch February 12, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[fix] 아카이빙, 업로드 QA 사항 반영

2 participants