Skip to content

Conversation

@m6z1
Copy link
Member

@m6z1 m6z1 commented Jan 10, 2026

📌𝘐𝘴𝘴𝘶𝘦𝘴

📎𝘞𝘰𝘳𝘬 𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯

  • 빌드 되는 거 확인했습니닷

📷𝘚𝘤𝘳𝘦𝘦𝘯𝘴𝘩𝘰𝘵

💬𝘛𝘰 𝘙𝘦𝘷𝘪𝘦𝘸𝘦𝘳𝘴

Summary by CodeRabbit

  • 새로운 기능
    • 이미지 처리 강화 — 자동 이미지 다운로드 및 압축 지원으로 업로드/첨부가 더 원활해짐
    • 피드 기능 확장 — 피드 조회, 생성, 수정, 삭제, 좋아요 및 공개/스포일러 설정 지원
    • 댓글 기능 추가 — 댓글 작성, 수정, 삭제 및 댓글 관련 플래그 관리
    • 추천 콘텐츠 강화 — 인기 피드 및 관심사 기반 피드 제공
    • 도메인·데이터 연동 개선 — 피드 관련 모델·캐시·매핑으로 성능·일관성 향상

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Walkthrough

피드 도메인 및 데이터 계층을 추가하고, 이미지 다운로드·압축 유틸리티, multipart 변환기, 네트워크 DTO 가시성 변경 및 feed 관련 매퍼와 포괄적 FeedRepository/유스케이스를 도입했습니다. (50단어 이내)

Changes

Cohort / File(s) 변경 사항
이미지 처리 유틸리티
core/common/src/main/java/com/into/websoso/core/common/image/ImageCompressor.kt, core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt
이미지 URI 일괄 압축 기능 추가 (ImageCompressor), URL → 캐시 Uri 다운로드 기능 추가 (ImageDownloader)
멀티파트 변환 & API 가시성
core/network/src/main/java/com/into/websoso/core/network/datasource/feed/mapper/MultiPartMapper.kt, core/network/src/main/java/com/into/websoso/core/network/datasource/feed/FeedApi.kt
제네릭/URI → Multipart 변환기 추가 (MultiPartMapper), FeedApi 가시성 internal → public 변경
요청 DTO
core/network/.../model/request/FeedRequestDto.kt, .../CommentRequestDto.kt
신규 FeedRequestDto 추가(직렬화), CommentRequestDto 가시성 internal → public 변경
응답 DTO 가시성 변경
core/network/.../model/response/*
여러 Feed 관련 응답 DTO(7개) 가시성 internal → public으로 변경
데이터 계층 엔티티
data/feed/src/main/java/com/into/websoso/data/feed/model/*
FeedEntity, FeedsEntity, CommentEntity, CommentsEntity, PopularFeedsEntity, UserInterestFeedsEntity 등 데이터 엔티티 다수 추가
DTO → 엔티티 매퍼
data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
네트워크 DTO들을 데이터 엔티티로 변환하는 확장 함수 7개 추가
피드 저장소
data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt
캐싱 포함 포괄적 싱글톤 저장소 추가: 피드 CRUD, 좋아요·댓글·플래그, 이미지 다운로드·압축 연동 등 다수 suspend 함수 도입
도메인 모듈 & 모델
domain/feed/..., settings.gradle.kts, domain/feed/build.gradle.kts, domain/feed/.gitignore
:domain:feed 모듈 추가, 도메인 모델(Feed, Feeds) 및 GetFeedsUseCase 추가, 빌드 설정/의존성 업데이트
모듈 의존성
data/feed/build.gradle.kts
core.networkdata.library 의존성 추가

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FeedRepository
    participant ImageDownloader
    participant ImageCompressor
    participant MultiPartMapper
    participant FeedApi

    Client->>FeedRepository: saveFeed(categories, content, novelId, images...)
    FeedRepository->>ImageDownloader: formatImageToUri(url) / downloadImage(...)
    ImageDownloader-->>FeedRepository: Result<Uri>
    FeedRepository->>ImageCompressor: compressImages(imageUris)
    ImageCompressor-->>FeedRepository: List<Uri> (compressed)
    FeedRepository->>MultiPartMapper: formatToMultipart(feedData)
    MultiPartMapper-->>FeedRepository: MultipartBody.Part (feed)
    FeedRepository->>MultiPartMapper: formatToMultipart(imageUri) for each image
    MultiPartMapper-->>FeedRepository: MultipartBody.Part (images)
    FeedRepository->>FeedApi: saveFeed(multipart request)
    FeedApi-->>FeedRepository: FeedResponseDto
    FeedRepository->>FeedRepository: Update cachedFeeds
    FeedRepository-->>Client: Success / Result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🍯 [FEAT], 🏹 궁사 명지

Suggested reviewers

  • s9hn
  • yeonjeen

Poem

🐰 캐시 속 당근 물고
이미지를 쏙 줄였네
다운로드하고 묶어 보내니
피드가 말끔히 자라나네
토끼 발자국으로 축하해요 🥕🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명이 템플릿 구조를 따르고 있으나, '워크 설명' 섹션이 매우 간략하여 변경사항에 대한 충분한 정보가 부족합니다. 워크 설명을 더욱 상세히 작성하여 주요 변경사항, 새로 생성된 모듈들, 그리고 의존성 변화에 대해 설명해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 반영하고 있으며, '피드 데이터 레이어 모듈 분리'는 코드 변경의 핵심 목표와 일치합니다.
Linked Issues check ✅ Passed 코드 변경사항이 linked issue #779의 목표인 '피드 데이터 레이어 모듈 분리'를 완벽하게 충족합니다.
Out of Scope Changes check ✅ Passed 모든 코드 변경사항이 피드 데이터 레이어 모듈 분리라는 명확한 범위 내에 있으며, 범위를 벗어난 변경은 없습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@github-actions github-actions bot requested a review from s9hn January 10, 2026 07:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In
@core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt:
- Around line 23-27: The code opens an HttpURLConnection via
URL(url).openConnection() and then calls connection.connect() without timeouts
or response-code checks; update the logic around the connection variable to set
connection.connectTimeout and connection.readTimeout (e.g., 5_000 and 10_000 ms
or configurable), call connection.connect(), then immediately verify
connection.responseCode is in the 200..299 range and return
Result.failure(IOException(...)) for non-success codes, and ensure the
connection is properly disconnected in a finally block (or use
try-with-resources equivalent) around the runCatching block so slow/failed
endpoints don't hang and error responses aren't treated as valid images.
- Around line 21-38: The formatImageToUri function currently downloads any URL
without validating content type or size; update it to first check the
HttpURLConnection's Content-Type header starts with "image/" and return a
failure if not, enforce a max allowed size (e.g., via
connection.contentLengthLong when available and/or by counting bytes while
copying the inputStream) and abort/return failure if the size exceeds the limit,
and ensure you still close/disconnect the connection; keep using the existing
connection, inputStream, File.createTempFile and Uri.fromFile logic but add
these validations and return appropriate Result.failure when checks fail.

In
@core/network/src/main/java/com/into/websoso/core/network/datasource/feed/mapper/MultiPartMapper.kt:
- Around line 19-27: The function formatToMultipart uses
Json.encodeToString(target) but has no compile-time guarantee that T is
@Serializable, so either change the signature to require an explicit serializer
(e.g., add a parameter serializer: KSerializer<T> and call
Json.encodeToString(serializer, target)) to ensure correct serialization at
compile time, or keep the signature but wrap the Json.encodeToString call in a
try/catch that throws or logs a clear error mentioning formatToMultipart and
that T must be @Serializable; also update KDoc for formatToMultipart to state
the requirement if you choose the runtime-check option.
- Around line 29-43: The default fallback fileName "file.jpg" in
formatToMultipart should be replaced with a more accurate name/extension: try to
resolve the original filename from the Uri via ContentResolver query
(OpenableColumns.DISPLAY_NAME) inside formatToMultipart, and if that's null,
derive an extension from the contentType using
MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType) and use a
fallback like "file.<ext>" (or "file.bin" if extension resolution fails); update
references to fileName and ensure multipart creation via
MultipartBody.Part.createFormData(partName, fileName, requestBody) uses the new
name.

In @data/feed/build.gradle.kts:
- Around line 14-15: The core.network module currently depends on data-layer
modules (data.account, data.library); remove those data-layer dependencies from
the dependencies block in core/network/build.gradle.kts (delete the
implementation(projects.data.account) and implementation(projects.data.library)
entries referenced around lines 31-32) and instead expose required contracts
from the core layer or create a dedicated core-network API module (e.g.,
core.network.api) for abstractions that core.network needs; move concrete
implementations into the data layer so data.feed/data.library depend on
core/network interfaces rather than core.network depending on data modules.

In @data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt:
- Around line 24-50: In FeedResponseDto.toData() the images list is built from
thumbnailUrl but imageCount is taken from the DTO, causing mismatches; change
the mapping to first build a local imagesList = thumbnailUrl?.let { listOf(it) }
?: emptyList(), set images = imagesList and set imageCount = imagesList.size
(instead of using the DTO imageCount) so the mapped FeedEntity (in
FeedResponseDto.toData()) is consistent with FeedDetailResponseDto.toData().

In
@data/feed/src/main/java/com/into/websoso/data/feed/model/PopularFeedsEntity.kt:
- Around line 6-12: Rename the misspelled property feesContent to feedContent in
the data class PopularFeedEntity and update all places that reference it
(constructor calls, property accessors, equality/hash, etc.); then update the
mapping logic in FeedMapper (both data/feed/.../FeedMapper.kt and
app/.../FeedMapper.kt) to map DTO.feedContent → PopularFeedEntity.feedContent
instead of DTO.feedContent → feesContent; finally update UI usage in
PopularFeedsViewHolder (and any other consumers) to use feedContent. Ensure
method/constructor signatures and any destructuring or JSON/parcel serialization
references are adjusted to the new property name to avoid build errors.

In
@data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt:
- Around line 51-84: Replace hardcoded "feed" and "feed.json" usages with the
existing constants to ensure consistency: in saveFeed (and the other
feed-related methods around feedApi.postFeed /
multiPartMapper.formatToMultipart), use PART_NAME_FEED for the partName argument
and FILE_NAME_FEED for the fileName argument instead of literal strings; update
any images multipart formatting calls that currently hardcode those values to
pass the same constants. Ensure all occurrences (including the mapping over
images and any other feed multipart calls) reference PART_NAME_FEED and
FILE_NAME_FEED so the constants defined later are actually used.

In @domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt:
- Around line 3-4: GetFeedsUseCase 클래스가 비어 있어 실제 피드를 가져오지 않으니 FeedRepository를
생성자 주입받아 피드 조회 로직을 구현하세요; 예컨대 클래스 GetFeedsUseCase(private val feedRepository:
FeedRepository)로 생성자를 추가하고, public suspend fun execute(params...): List<Feed> 또는
fun execute(): Flow<List<Feed>> 같은 명확한 진입 메서드를 만들어 내부에서 feedRepository의 적절한
메서드(예: getFeeds(), fetchFeeds(), observeFeeds() 등 실제 repository 시그니처에 맞는 메서드)를
호출해 결과를 반환하도록 연결하고 에러는 호출부가 처리할 수 있게 예외를 전파하거나 Result/Resource 래퍼로 감싸 반환하도록
구현하세요.
🧹 Nitpick comments (3)
core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt (1)

29-33: 임시 파일 정리 메커니즘 고려 필요

File.createTempFile로 생성된 임시 파일이 자동으로 삭제되지 않습니다. 캐시 디렉토리에 생성되므로 시스템이 결국 정리하겠지만, 명시적인 정리 로직이나 문서화를 권장합니다.

다음 옵션을 고려하세요:

  • 사용 후 명시적으로 파일 삭제
  • 정기적인 캐시 정리 정책 수립
  • 파일 생명주기에 대한 문서화
core/common/src/main/java/com/into/websoso/core/common/image/ImageCompressor.kt (1)

22-53: 압축 로직 검토 필요

이미지 압축 로직이 전반적으로 잘 구현되어 있으나, 몇 가지 개선 가능한 부분이 있습니다:

  1. Line 47: ByteArrayOutputStream.close()는 불필요합니다. ByteArrayOutputStream은 메모리 기반이며 닫을 리소스가 없어 close()가 no-op입니다.

  2. Line 36 압축 종료 조건: 품질이 QUALITY_DECREMENT_STEP 이하로 떨어지면 루프가 종료되지만, 이미지가 여전히 목표 크기보다 클 수 있습니다. 이 경우 큰 이미지가 그대로 저장되는데, 이것이 의도된 동작인지 확인이 필요합니다.

  3. 에러 처리: printStackTrace()로 에러를 출력하지만 조용히 무시됩니다. 프로덕션 환경에서 디버깅을 어렵게 만들 수 있으니, 적절한 로깅 시스템(예: Timber) 사용을 고려해보세요.

♻️ 개선 제안
-                        outputStream.close()
                         Uri.fromFile(compressedFile)
                     }.onFailure {
-                        it.printStackTrace()
+                        // TODO: Replace with proper logging (e.g., Timber.e(it))
+                        it.printStackTrace()
                     }.getOrNull()
data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt (1)

33-35: 불필요한 isEmpty 체크

clear() 메서드는 빈 리스트에 호출해도 안전하므로, isNotEmpty 체크가 불필요합니다.

♻️ 간소화 제안
     fun clearCachedFeeds() {
-        if (cachedFeeds.isNotEmpty()) _cachedFeeds.clear()
+        _cachedFeeds.clear()
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80d78a2 and 520d724.

📒 Files selected for processing (26)
  • core/common/src/main/java/com/into/websoso/core/common/image/ImageCompressor.kt
  • core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/FeedApi.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/mapper/MultiPartMapper.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/request/CommentRequestDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/request/FeedRequestDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/CommentResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/CommentsResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedDetailResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedsResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/PopularFeedsResponseDto.kt
  • core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt
  • data/feed/build.gradle.kts
  • data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/CommentEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/CommentsEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/FeedEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/FeedsEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/PopularFeedsEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt
  • data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt
  • domain/feed/.gitignore
  • domain/feed/build.gradle.kts
  • domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt
  • settings.gradle.kts
🧰 Additional context used
🧬 Code graph analysis (1)
data/feed/build.gradle.kts (1)
build-logic/src/main/kotlin/com/into/websoso/WebsosoDependenciesExtensions.kt (1)
  • implementation (70-76)
🪛 detekt (1.23.8)
domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt

[warning] 3-4: The class or object GetFeedsUseCase is empty.

(detekt.empty-blocks.EmptyClassBlock)

🔇 Additional comments (21)
core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/request/CommentRequestDto.kt (1)

7-7: 모듈화를 위한 필수적인 가시성 변경입니다.

internal 수정자 제거로 클래스를 public으로 변경한 것은 피드 데이터 레이어 모듈 분리에 필요합니다. data/feed 모듈의 FeedRepository에서 이 DTO를 import하여 사용하고 있으며, 같은 패턴으로 다른 응답 DTO들(FeedResponseDto, FeedsResponseDto 등)도 공개되어 있습니다. 모듈 계층 구조(core/network → data/feed)가 올바르게 유지되고 있습니다.

domain/feed/.gitignore (1)

1-1: LGTM! 표준 빌드 출력 제외 규칙입니다.

새로운 모듈의 빌드 디렉토리를 gitignore에 추가한 것은 적절한 설정입니다.

settings.gradle.kts (1)

48-48: LGTM! 새로운 도메인 모듈이 올바르게 추가되었습니다.

domain:feed 모듈이 적절한 위치에 포함되었으며, 기존 모듈 구조 패턴을 따르고 있습니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/PopularFeedsResponseDto.kt (1)

7-24: 데이터 레이어 내 적절한 모듈 간 접근입니다.

PopularFeedsResponseDtopublic으로 변경한 것은 data/feed 모듈의 FeedMapper에서 PopularFeedsEntity로 변환하기 위해 필요합니다. 네트워크 DTO는 core/network의 API와 data/feed의 매퍼에서만 사용되며, 도메인이나 프레젠테이션 레이어로는 누출되지 않습니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedResponseDto.kt (1)

7-48: 공개 API 노출 범위는 적절히 관리되고 있습니다.

FeedResponseDto의 가시성 변경은 필요하며, 실제로 적절히 구성되어 있습니다. data:feed 모듈의 FeedMapper에서만 사용되며, 즉시 FeedEntity(도메인 모델)로 변환되므로 상위 레이어로의 노출이 차단됩니다. 추가 조치는 불필요합니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedDetailResponseDto.kt (1)

7-7: 모듈 분리를 위한 적절한 가시성 변경입니다.

internal에서 public으로 변경하여 data 레이어의 매퍼가 이 DTO에 접근할 수 있도록 했습니다. 모듈 분리 작업의 일환으로 적절한 변경이지만, 이제 네트워크 DTO가 public API의 일부가 되었으므로 향후 변경 시 하위 호환성에 유의해야 합니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/CommentResponseDto.kt (1)

7-7: 모듈 분리를 위한 적절한 가시성 변경입니다.

FeedDetailResponseDto와 동일하게, 데이터 레이어의 매퍼에서 접근할 수 있도록 가시성을 public으로 변경했습니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedsResponseDto.kt (1)

7-7: 모듈 분리를 위한 적절한 가시성 변경입니다.

다른 DTO들과 일관되게 가시성을 public으로 변경하여 데이터 레이어에서 사용할 수 있도록 했습니다.

domain/feed/build.gradle.kts (1)

1-13: GetFeedsUseCase는 현재 비어있으며 외부 의존성이 필요하지 않습니다.

GetFeedsUseCase.kt을 확인한 결과, 클래스 정의만 있고 구현이 없는 상태입니다. 따라서 현재로서는 외부 의존성을 선언할 필요가 없습니다. 다만 유스케이스가 실제로 구현될 때 데이터 레이어나 다른 모듈에 대한 의존성이 필요하다면, 그 시점에 dependencies 블록을 추가하면 됩니다.

Likely an incorrect or invalid review comment.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/CommentsResponseDto.kt (1)

7-7: 가시성 변경 승인

데이터 레이어에서 DTO에 접근할 수 있도록 internal에서 public으로 가시성을 변경한 것이 적절합니다. 모듈 분리 목표에 부합합니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/FeedApi.kt (1)

20-20: API 인터페이스 가시성 변경 승인

데이터 레이어의 FeedRepository에서 주입받을 수 있도록 FeedApi 인터페이스를 public으로 변경한 것이 적절합니다.

data/feed/src/main/java/com/into/websoso/data/feed/model/CommentsEntity.kt (1)

1-6: 데이터 엔티티 모델 승인

네트워크 응답 DTO를 데이터 레이어 엔티티로 매핑하기 위한 명확한 모델입니다. 구조가 간결하고 목적에 부합합니다.

data/feed/src/main/java/com/into/websoso/data/feed/model/FeedsEntity.kt (1)

1-7: LGTM!

피드 목록을 담는 데이터 클래스 구조가 명확하고 적절합니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/UserInterestFeedsResponseDto.kt (1)

7-7: LGTM!

모듈 분리를 위해 가시성을 public으로 변경한 것이 적절합니다. 데이터 레이어에서 네트워크 DTO를 매핑하는 데 필요한 변경사항입니다.

data/feed/src/main/java/com/into/websoso/data/feed/model/CommentEntity.kt (1)

1-13: LGTM!

댓글 데이터를 표현하는 엔티티 구조가 잘 정의되어 있습니다. 필요한 모든 속성(사용자 정보, 댓글 내용, 플래그들)을 적절히 포함하고 있습니다.

core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/request/FeedRequestDto.kt (1)

1-18: LGTM!

피드 요청을 위한 DTO 구조가 명확하고 직렬화 어노테이션이 올바르게 적용되어 있습니다. nullable 처리도 적절합니다.

data/feed/src/main/java/com/into/websoso/data/feed/model/UserInterestFeedsEntity.kt (1)

1-29: LGTM!

데이터 엔티티 구조가 명확하고, fromMessage 함수도 안전하게 nullable 타입을 반환하도록 잘 구현되어 있습니다.

data/feed/src/main/java/com/into/websoso/data/feed/model/FeedEntity.kt (1)

1-32: LGTM!

피드 데이터 모델이 명확하게 구조화되어 있고, 중첩된 엔티티(UserEntity, NovelEntity)를 통해 관련 데이터를 효과적으로 그룹화했습니다. NovelEntity의 nullable 필드들은 소설 정보가 선택적일 수 있는 상황을 적절히 반영하고 있습니다.

data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt (1)

103-114: 프로퍼티 이름 확인 필요

Line 108의 feesContent가 오타인지 확인이 필요합니다. 다른 곳에서는 모두 feedContent를 사용하는데, 여기만 feesContent로 되어 있습니다.

PopularFeedsEntity.PopularFeedEntity의 실제 프로퍼티 이름이 feesContent인지, 아니면 feedContent가 되어야 하는지 확인해주세요.

data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt (2)

37-50: 캐시 동작 검증 필요

fetchFeeds를 호출할 때마다 새로운 피드가 _cachedFeeds에 계속 추가됩니다(Line 48). 카테고리를 변경하거나 같은 카테고리를 여러 번 조회하면 중복된 피드가 캐시에 쌓일 수 있습니다.

이것이 의도된 동작인지 확인이 필요합니다:

  • 페이지네이션을 위해 누적하는 것이 목적인가요?
  • 카테고리별로 캐시를 분리해야 하나요?
  • 중복 피드 처리 로직이 필요한가요?

128-213: LGTM!

피드 조회, 삭제, 좋아요, 댓글 관련 기능들이 명확하게 구현되어 있습니다. 각 메서드의 책임이 잘 분리되어 있고, 로컬 데이터와 원격 데이터의 동기화도 적절히 처리하고 있습니다.

Comment on lines +21 to +38
suspend fun formatImageToUri(url: String): Result<Uri> =
withContext(Dispatchers.IO) {
val connection = (URL(url).openConnection() as? HttpURLConnection)
?: return@withContext Result.failure(IllegalStateException("Invalid HTTP connection"))

runCatching {
connection.connect()
connection.inputStream.use { inputStream ->
val file = File.createTempFile("image_", ".jpg", context.cacheDir)
FileOutputStream(file).use { output ->
inputStream.copyTo(output)
}
Uri.fromFile(file)
}
}.also {
connection.disconnect()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

다운로드 파일 크기 제한 및 Content-Type 검증 권장

임의의 URL에서 제한 없이 이미지를 다운로드하면 다음 위험이 있습니다:

  • 대용량 파일 다운로드로 인한 메모리/저장소 부족
  • 이미지가 아닌 파일 타입 처리

파일 크기 제한과 Content-Type 검증을 추가하는 것을 권장합니다.

💡 파일 크기 및 타입 검증 추가
 connection.connect()
 
 val responseCode = connection.responseCode
 if (responseCode !in 200..299) {
     return@withContext Result.failure(
         IllegalStateException("HTTP error code: $responseCode")
     )
 }
 
+val contentLength = connection.contentLength
+val maxSize = 10 * 1024 * 1024  // 10MB
+if (contentLength > maxSize) {
+    return@withContext Result.failure(
+        IllegalStateException("File too large: $contentLength bytes")
+    )
+}
+
+val contentType = connection.contentType
+if (contentType?.startsWith("image/") != true) {
+    return@withContext Result.failure(
+        IllegalStateException("Invalid content type: $contentType")
+    )
+}
+
 connection.inputStream.use { inputStream ->
🤖 Prompt for AI Agents
In
@core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt
around lines 21 - 38, The formatImageToUri function currently downloads any URL
without validating content type or size; update it to first check the
HttpURLConnection's Content-Type header starts with "image/" and return a
failure if not, enforce a max allowed size (e.g., via
connection.contentLengthLong when available and/or by counting bytes while
copying the inputStream) and abort/return failure if the size exceeds the limit,
and ensure you still close/disconnect the connection; keep using the existing
connection, inputStream, File.createTempFile and Uri.fromFile logic but add
these validations and return appropriate Result.failure when checks fail.

Comment on lines +23 to +27
val connection = (URL(url).openConnection() as? HttpURLConnection)
?: return@withContext Result.failure(IllegalStateException("Invalid HTTP connection"))

runCatching {
connection.connect()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

연결 타임아웃 설정이 누락되어 네트워크 지연 시 무한 대기 가능

HttpURLConnection에 타임아웃이 설정되지 않아 네트워크 응답이 느리거나 서버가 응답하지 않을 때 스레드가 무한정 차단될 수 있습니다. 또한 HTTP 응답 코드를 확인하지 않아 오류 응답(4xx, 5xx)의 내용도 이미지로 저장될 수 있습니다.

🔧 타임아웃 및 응답 코드 검증 추가
 val connection = (URL(url).openConnection() as? HttpURLConnection)
     ?: return@withContext Result.failure(IllegalStateException("Invalid HTTP connection"))

 runCatching {
+    connection.connectTimeout = 10_000  // 10초
+    connection.readTimeout = 10_000     // 10초
     connection.connect()
+    
+    val responseCode = connection.responseCode
+    if (responseCode !in 200..299) {
+        return@withContext Result.failure(
+            IllegalStateException("HTTP error code: $responseCode")
+        )
+    }
+    
     connection.inputStream.use { inputStream ->
🤖 Prompt for AI Agents
In
@core/network/src/main/java/com/into/websoso/core/network/common/ImageDownloader.kt
around lines 23 - 27, The code opens an HttpURLConnection via
URL(url).openConnection() and then calls connection.connect() without timeouts
or response-code checks; update the logic around the connection variable to set
connection.connectTimeout and connection.readTimeout (e.g., 5_000 and 10_000 ms
or configurable), call connection.connect(), then immediately verify
connection.responseCode is in the 200..299 range and return
Result.failure(IOException(...)) for non-success codes, and ensure the
connection is properly disconnected in a finally block (or use
try-with-resources equivalent) around the runCatching block so slow/failed
endpoints don't hang and error responses aren't treated as valid images.

Comment on lines +19 to +27
inline fun <reified T> formatToMultipart(
target: T,
partName: String = "data",
fileName: String = "data.json",
): MultipartBody.Part {
val json = Json.encodeToString(target)
val requestBody = json.toRequestBody("application/json".toMediaType())
return MultipartBody.Part.createFormData(partName, fileName, requestBody)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

제네릭 타입 제약 조건 추가 필요

Json.encodeToString(target)를 사용하려면 타입 T@Serializable이어야 하지만, 현재 타입 파라미터에 제약 조건이 없습니다. 이로 인해 직렬화 불가능한 타입이 전달될 경우 런타임 오류가 발생합니다.

🔧 타입 제약 추가 제안

옵션 1: 인라인 함수이므로 컴파일러가 직렬화 가능 여부를 확인하도록 문서화하거나, 옵션 2: 직렬화 실패 시 예외 처리 추가:

 inline fun <reified T> formatToMultipart(
     target: T,
     partName: String = "data",
     fileName: String = "data.json",
 ): MultipartBody.Part {
-    val json = Json.encodeToString(target)
+    val json = try {
+        Json.encodeToString(target)
+    } catch (e: Exception) {
+        throw IllegalArgumentException("직렬화 불가능한 타입: ${T::class.simpleName}", e)
+    }
     val requestBody = json.toRequestBody("application/json".toMediaType())
     return MultipartBody.Part.createFormData(partName, fileName, requestBody)
 }

또는 KDoc에 @param T must be annotated with @Serializable 명시

🤖 Prompt for AI Agents
In
@core/network/src/main/java/com/into/websoso/core/network/datasource/feed/mapper/MultiPartMapper.kt
around lines 19 - 27, The function formatToMultipart uses
Json.encodeToString(target) but has no compile-time guarantee that T is
@Serializable, so either change the signature to require an explicit serializer
(e.g., add a parameter serializer: KSerializer<T> and call
Json.encodeToString(serializer, target)) to ensure correct serialization at
compile time, or keep the signature but wrap the Json.encodeToString call in a
try/catch that throws or logs a clear error mentioning formatToMultipart and
that T must be @Serializable; also update KDoc for formatToMultipart to state
the requirement if you choose the runtime-check option.

Comment on lines +29 to +43
fun formatToMultipart(
uri: Uri,
partName: String = "images",
contentType: String = "image/*",
): MultipartBody.Part {
val inputStream = context.contentResolver.openInputStream(uri)
?: throw IllegalArgumentException("유효하지 않은 URI: $uri")
return inputStream.use { stream ->
val bytes = stream.readBytes()
val fileName = uri.lastPathSegment ?: "file.jpg"
val requestBody = bytes.toRequestBody(contentType.toMediaType())
MultipartBody.Part.createFormData(partName, fileName, requestBody)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for formatToMultipart usage
rg -n "formatToMultipart" --type=kt -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 90


🏁 Script executed:

# Search for ImageCompressor usage
rg -n "ImageCompressor" --type=kt -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 90


🏁 Script executed:

# Look for FeedRepository and feed upload patterns
fd "FeedRepository" --type=kt --exec cat -n {}

Repository: Team-WSS/WSS-Android

Length of output: 233


🏁 Script executed:

# Check for any file size validation or limits
rg -n "size|Size|limit|Limit|MAX|max" --type=kt core/network/src/main/java/com/into/websoso/core/network/datasource/feed/ -B 1 -A 1

Repository: Team-WSS/WSS-Android

Length of output: 90


🏁 Script executed:

# Search for formatToMultipart usage in .kt files
rg -n "formatToMultipart" -g '*.kt' -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 7362


🏁 Script executed:

# Search for ImageCompressor usage in .kt files
rg -n "ImageCompressor" -g '*.kt' -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 3412


🏁 Script executed:

# Find the MultiPartMapper file location to understand context better
find . -name "MultiPartMapper.kt" -type f

Repository: Team-WSS/WSS-Android

Length of output: 232


🏁 Script executed:

# Search for FeedRepository in .kt files
rg -n "class FeedRepository|object FeedRepository" -g '*.kt' -A 10

Repository: Team-WSS/WSS-Android

Length of output: 2719


🏁 Script executed:

# View the postFeed method in FeedRepository to see complete flow
sed -n '50,85p' data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt

Repository: Team-WSS/WSS-Android

Length of output: 1433


🏁 Script executed:

# View the putFeed method in FeedRepository
sed -n '88,120p' data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt

Repository: Team-WSS/WSS-Android

Length of output: 1458


🏁 Script executed:

# Check how images parameter is processed before being used
rg -n "fun postFeed|fun putFeed" -g '*.kt' data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt -A 30

Repository: Team-WSS/WSS-Android

Length of output: 46


🏁 Script executed:

# Check if compress is called on images anywhere
rg -n "imageCompressor.compress|compress\(" -g '*.kt' data/feed/src/main/java/com/into/websoso/data/feed/repository/ -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 733


🏁 Script executed:

# Search for usages of compressImages
rg -n "compressImages" -g '*.kt' -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 1995


🏁 Script executed:

# Check for any file size validation or limits in feed API or mapper
rg -n "MAX|max|size|Size|limit" -g '*.kt' core/network/src/main/java/com/into/websoso/core/network/datasource/feed/ -B 1 -A 1

Repository: Team-WSS/WSS-Android

Length of output: 429


🏁 Script executed:

# Check ImageCompressor implementation for context on compression
cat -n core/common/src/main/java/com/into/websoso/core/common/image/ImageCompressor.kt

Repository: Team-WSS/WSS-Android

Length of output: 2876


🏁 Script executed:

# Check CreateFeedViewModel to see full image handling flow
rg -n "saveFeed\|editFeed" -g '*.kt' app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt -B 5 -A 5

Repository: Team-WSS/WSS-Android

Length of output: 46


🏁 Script executed:

# Verify that attachedImages (compressed) are what's passed to saveFeed
rg -n "_attachedImages\|attachedImages" -g '*.kt' app/src/main/java/com/into/websoso/ui/createFeed/CreateFeedViewModel.kt -B 2 -A 2

Repository: Team-WSS/WSS-Android

Length of output: 46


🏁 Script executed:

# Find CreateFeedViewModel file
find . -name "CreateFeedViewModel.kt" -type f

Repository: Team-WSS/WSS-Android

Length of output: 137


🏁 Script executed:

# Search for where saveFeed is called in the codebase
rg -n "\.saveFeed\(" -g '*.kt' -B 3 -A 3

Repository: Team-WSS/WSS-Android

Length of output: 920


🏁 Script executed:

# Check the complete flow - search for attachedImages usage more broadly
rg -n "attachedImages" -g '*.kt' -B 2 -A 2 | head -50

Repository: Team-WSS/WSS-Android

Length of output: 5422


파일명 기본값 개선 고려

Line 39의 기본 파일명 "file.jpg"는 contentType이 다양한 파일 타입을 지원할 수 있을 때 부적절할 수 있습니다. contentType에 따라 적절한 확장자를 선택하거나, 원본 파일 정보가 손실되지 않도록 개선하는 것을 검토하세요.

참고: readBytes()로 인한 메모리 문제는 발생하지 않습니다. CreateFeedViewModel에서 이미지가 ImageCompressor를 통해 약 250KB로 압축된 후 전달되므로, 메모리 사용량은 허용 범위 내입니다.

🤖 Prompt for AI Agents
In
@core/network/src/main/java/com/into/websoso/core/network/datasource/feed/mapper/MultiPartMapper.kt
around lines 29 - 43, The default fallback fileName "file.jpg" in
formatToMultipart should be replaced with a more accurate name/extension: try to
resolve the original filename from the Uri via ContentResolver query
(OpenableColumns.DISPLAY_NAME) inside formatToMultipart, and if that's null,
derive an extension from the contentType using
MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType) and use a
fallback like "file.<ext>" (or "file.bin" if extension resolution fails); update
references to fileName and ensure multipart creation via
MultipartBody.Part.createFormData(partName, fileName, requestBody) uses the new
name.

Comment on lines +14 to +15
implementation(projects.core.network)
implementation(projects.data.library)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(core/network|data/library|data/feed)" | head -20

Repository: Team-WSS/WSS-Android

Length of output: 1800


🏁 Script executed:

find . -name "build.gradle.kts" -type f | grep -E "(core|data)" | head -20

Repository: Team-WSS/WSS-Android

Length of output: 420


🏁 Script executed:

cat -n data/feed/build.gradle.kts

Repository: Team-WSS/WSS-Android

Length of output: 534


🏁 Script executed:

cat -n core/network/build.gradle.kts

Repository: Team-WSS/WSS-Android

Length of output: 1406


🏁 Script executed:

cat -n data/library/build.gradle.kts

Repository: Team-WSS/WSS-Android

Length of output: 439


모듈 의존성 및 순환 의존성 검증 완료

피드 데이터 레이어 분리에 필요한 core.networkdata.library 모듈 의존성이 적절하게 추가되었습니다(라인 14-15). 확인 결과 core.networkdata.library 모두 data.feed를 의존하지 않으므로 순환 의존성이 없습니다.

다만, 아키텍처 레이어 위반 문제를 발견했습니다:

  • core.networkdata.account, data.library 등 데이터 레이어 모듈에 의존 중
  • 클린 아키텍처 원칙에서 코어 레이어는 데이터 레이어에 의존하면 안 됨 (데이터 레이어 → 코어 레이어 방향이어야 함)
  • core/network/build.gradle.kts의 31-32번 라인에서 데이터 레이어 의존성 제거 필요
🤖 Prompt for AI Agents
In @data/feed/build.gradle.kts around lines 14 - 15, The core.network module
currently depends on data-layer modules (data.account, data.library); remove
those data-layer dependencies from the dependencies block in
core/network/build.gradle.kts (delete the implementation(projects.data.account)
and implementation(projects.data.library) entries referenced around lines 31-32)
and instead expose required contracts from the core layer or create a dedicated
core-network API module (e.g., core.network.api) for abstractions that
core.network needs; move concrete implementations into the data layer so
data.feed/data.library depend on core/network interfaces rather than
core.network depending on data modules.

Comment on lines +24 to +50
fun FeedResponseDto.toData(): FeedEntity =
FeedEntity(
user = FeedEntity.UserEntity(
id = userId,
nickname = nickname,
avatarImage = avatarImage,
),
createdDate = createdDate,
id = feedId,
content = feedContent,
relevantCategories = relevantCategories,
likeCount = likeCount,
isLiked = isLiked,
commentCount = commentCount,
isModified = isModified,
isSpoiler = isSpoiler,
isMyFeed = isMyFeed,
isPublic = isPublic,
images = thumbnailUrl?.let { listOf(it) } ?: emptyList(),
imageCount = imageCount,
novel = FeedEntity.NovelEntity(
id = novelId,
title = title,
rating = novelRating,
ratingCount = novelRatingCount,
),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "FeedMapper.kt" -type f

Repository: Team-WSS/WSS-Android

Length of output: 326


🏁 Script executed:

cat -n ./data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt

Repository: Team-WSS/WSS-Android

Length of output: 5558


🏁 Script executed:

fd "FeedResponseDto|FeedDetailResponseDto" --type f --extension kt | head -20

Repository: Team-WSS/WSS-Android

Length of output: 519


🏁 Script executed:

cat -n core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedResponseDto.kt

Repository: Team-WSS/WSS-Android

Length of output: 1743


🏁 Script executed:

cat -n core/network/src/main/java/com/into/websoso/core/network/datasource/feed/model/response/FeedDetailResponseDto.kt

Repository: Team-WSS/WSS-Android

Length of output: 1673


imageCountimages 리스트 크기 불일치 문제

FeedResponseDto.toData()에서 imagesthumbnailUrl만 포함하여 최대 1개 항목이 되지만, imageCount는 DTO에서 직접 받아오고 있습니다. 예를 들어 서버의 imageCount가 3이고 thumbnailUrl만 전달된 경우, 실제 images 리스트 크기는 1이 됩니다.

반면 FeedDetailResponseDto.toData()에서는 imageCount = images.size로 일관되게 처리하고 있습니다. 두 매핑 함수의 일관성을 맞추기 위해, FeedResponseDto.toData()에서도 실제 이미지 개수를 반영하도록 수정이 필요합니다:

         images = thumbnailUrl?.let { listOf(it) } ?: emptyList(),
-        imageCount = imageCount,
+        imageCount = thumbnailUrl?.let { 1 } ?: 0,
🤖 Prompt for AI Agents
In @data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
around lines 24 - 50, In FeedResponseDto.toData() the images list is built from
thumbnailUrl but imageCount is taken from the DTO, causing mismatches; change
the mapping to first build a local imagesList = thumbnailUrl?.let { listOf(it) }
?: emptyList(), set images = imagesList and set imageCount = imagesList.size
(instead of using the DTO imageCount) so the mapped FeedEntity (in
FeedResponseDto.toData()) is consistent with FeedDetailResponseDto.toData().

Comment on lines +6 to +12
data class PopularFeedEntity(
val feedId: Long,
val feesContent: String,
val likeCount: Int,
val commentCount: Int,
val isSpoiler: Boolean,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and view its content
cat -n "data/feed/src/main/java/com/into/websoso/data/feed/model/PopularFeedsEntity.kt" 2>/dev/null || echo "File not found at expected path, searching for it..."

# Search for the file if not at expected location
fd -e "PopularFeedsEntity.kt" -o -e "PopularFeedEntity.kt"

Repository: Team-WSS/WSS-Android

Length of output: 604


🏁 Script executed:

# Search for PopularFeedResponseDto and similar DTOs
rg -i "PopularFeed" --type kotlin -A 10

# Check for feesContent usage
echo "=== feesContent occurrences ==="
rg "feesContent" --type kotlin

# Check for feedContent usage
echo "=== feedContent occurrences ==="
rg "feedContent" --type kotlin

Repository: Team-WSS/WSS-Android

Length of output: 50376


🏁 Script executed:

# Also check if there are any other entity/DTO files with similar naming patterns
echo "=== Files with PopularFeed* names ==="
fd -i "popularfeed"

# Check network/DTO layer structure
echo "=== Feed-related files in network/dto directories ==="
fd -e ".kt" | grep -i "network\|dto\|feed" | head -30

Repository: Team-WSS/WSS-Android

Length of output: 2949


feesContent 필드명 오타 수정 필요

Line 8의 feesContent는 오타입니다. 네트워크 DTO 계층의 PopularFeedsResponseDto에서는 feedContent로 정의되어 있으며, 매퍼에서 DTO의 feedContent를 Entity의 feesContent로 매핑하고 있습니다. 의미상 "fees"(요금)는 맞지 않으므로 feedContent로 통일해야 합니다.

수정사항
    data class PopularFeedEntity(
        val feedId: Long,
-       val feesContent: String,
+       val feedContent: String,
        val likeCount: Int,
        val commentCount: Int,
        val isSpoiler: Boolean,
    )

매퍼 파일도 함께 수정 필요:

  • data/feed/src/main/java/com/into/websoso/data/feed/mapper/FeedMapper.kt
  • app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt
PopularFeedsEntity.PopularFeedEntity(
    feedId = feed.feedId,
-   feesContent = feed.feedContent,
+   feedContent = feed.feedContent,
    ...
)

UI 레이어 (PopularFeedsViewHolder.kt) 사용처도 업데이트:

-tvPopularFeedContent.text = feed.feesContent
+tvPopularFeedContent.text = feed.feedContent
📝 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
data class PopularFeedEntity(
val feedId: Long,
val feesContent: String,
val likeCount: Int,
val commentCount: Int,
val isSpoiler: Boolean,
)
data class PopularFeedEntity(
val feedId: Long,
val feedContent: String,
val likeCount: Int,
val commentCount: Int,
val isSpoiler: Boolean,
)
🤖 Prompt for AI Agents
In
@data/feed/src/main/java/com/into/websoso/data/feed/model/PopularFeedsEntity.kt
around lines 6 - 12, Rename the misspelled property feesContent to feedContent
in the data class PopularFeedEntity and update all places that reference it
(constructor calls, property accessors, equality/hash, etc.); then update the
mapping logic in FeedMapper (both data/feed/.../FeedMapper.kt and
app/.../FeedMapper.kt) to map DTO.feedContent → PopularFeedEntity.feedContent
instead of DTO.feedContent → feesContent; finally update UI usage in
PopularFeedsViewHolder (and any other consumers) to use feedContent. Ensure
method/constructor signatures and any destructuring or JSON/parcel serialization
references are adjusted to the new property name to avoid build errors.

Comment on lines +51 to +84
suspend fun saveFeed(
relevantCategories: List<String>,
feedContent: String,
novelId: Long?,
isSpoiler: Boolean,
isPublic: Boolean,
images: List<Uri>,
) {
runCatching {
feedApi.postFeed(
feedRequestDto = multiPartMapper.formatToMultipart<FeedRequestDto>(
target = FeedRequestDto(
relevantCategories = relevantCategories,
feedContent = feedContent,
novelId = novelId,
isSpoiler = isSpoiler,
isPublic = isPublic,
),
partName = PART_NAME_FEED,
fileName = "feed.json",
),
images = images.map { multiPartMapper.formatToMultipart(it) },
)
}.onSuccess {
val novel = novelId?.let { id ->
libraryLocalDataSource.selectNovelByNovelId(id)
}

if (novel != null) {
val updatedNovel = novel.copy(myFeeds = listOf(feedContent) + novel.myFeeds)
libraryLocalDataSource.insertNovel(updatedNovel)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

상수 사용의 일관성 문제

Line 69에서는 PART_NAME_FEED 상수를 사용하는데, Line 107에서는 동일한 값을 하드코딩("feed")하고 있습니다.

또한 Lines 70, 108에서 "feed.json"을 하드코딩하고 있지만, Line 217에 정의된 FILE_NAME_FEED 상수는 전혀 사용되지 않고 있습니다.

♻️ 일관성 개선안
             feedApi.putFeed(
                 feedId = feedId,
                 feedRequestDto = multiPartMapper.formatToMultipart<FeedRequestDto>(
                     target = FeedRequestDto(
                         relevantCategories = relevantCategories,
                         feedContent = editedFeed,
                         novelId = novelId,
                         isSpoiler = isSpoiler,
                         isPublic = isPublic,
                     ),
-                    partName = "feed",
-                    fileName = "feed.json",
+                    partName = PART_NAME_FEED,
+                    fileName = FILE_NAME_FEED,
                 ),
                 images = images.map { multiPartMapper.formatToMultipart(it) },
             )

saveFeed 메서드(Line 70)도 동일하게 수정:

                 feedRequestDto = multiPartMapper.formatToMultipart<FeedRequestDto>(
                     target = FeedRequestDto(
                         relevantCategories = relevantCategories,
                         feedContent = feedContent,
                         novelId = novelId,
                         isSpoiler = isSpoiler,
                         isPublic = isPublic,
                     ),
                     partName = PART_NAME_FEED,
-                    fileName = "feed.json",
+                    fileName = FILE_NAME_FEED,
                 ),

Also applies to: 86-126

🤖 Prompt for AI Agents
In
@data/feed/src/main/java/com/into/websoso/data/feed/repository/FeedRepository.kt
around lines 51 - 84, Replace hardcoded "feed" and "feed.json" usages with the
existing constants to ensure consistency: in saveFeed (and the other
feed-related methods around feedApi.postFeed /
multiPartMapper.formatToMultipart), use PART_NAME_FEED for the partName argument
and FILE_NAME_FEED for the fileName argument instead of literal strings; update
any images multipart formatting calls that currently hardcode those values to
pass the same constants. Ensure all occurrences (including the mapping over
images and any other feed multipart calls) reference PART_NAME_FEED and
FILE_NAME_FEED so the constants defined later are actually used.

Copy link
Member

@s9hn s9hn left a comment

Choose a reason for hiding this comment

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

어프합니다!

@m6z1 m6z1 merged commit 06c5412 into develop Jan 10, 2026
0 of 2 checks passed
@m6z1 m6z1 deleted the feat/779 branch January 10, 2026 07:31
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt:
- Line 13: The mutable field previousCategory in GetFeedsUseCase is not
thread-safe and can cause races when the UseCase is singleton; remove
previousCategory from GetFeedsUseCase and delegate category state to the
repository (e.g., add a currentCategory property and update it inside the
repository's fetch method) so GetFeedsUseCase becomes stateless, or if you must
keep UseCase-owned state, protect access to previousCategory using a Mutex or
synchronize the getters/setters (or scope UseCase per category) to prevent
concurrent reads/writes; update all references to previousCategory in
GetFeedsUseCase and related call sites to use the repository's currentCategory
API or the new synchronized accessors.
🧹 Nitpick comments (1)
domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt (1)

29-29: 연산자 일관성 고려

Line 29에서 or 키워드를 사용하고 있는데, Kotlin에서는 일반적으로 boolean 연산에 ||를 사용합니다. Line 21의 &&와 일관성을 위해 || 사용을 권장합니다.

♻️ 제안하는 수정
-                    size = if (isFeedRefreshed or isCategorySwitched) INITIAL_REQUEST_SIZE else ADDITIONAL_REQUEST_SIZE,
+                    size = if (isFeedRefreshed || isCategorySwitched) INITIAL_REQUEST_SIZE else ADDITIONAL_REQUEST_SIZE,
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 520d724 and 324131e.

📒 Files selected for processing (5)
  • domain/feed/build.gradle.kts
  • domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt
  • domain/feed/src/main/java/com/into/websoso/feed/mapper/FeedMapper.kt
  • domain/feed/src/main/java/com/into/websoso/feed/model/Feed.kt
  • domain/feed/src/main/java/com/into/websoso/feed/model/Feeds.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • domain/feed/build.gradle.kts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
domain/feed/src/main/java/com/into/websoso/feed/model/Feed.kt (1)

1-32: 도메인 모델 구조가 명확합니다.

Feed 도메인 모델이 사용자 정보, 소설 정보, 메타데이터를 명확하게 구조화하고 있습니다. Novel의 nullable 속성들은 소설 정보가 선택적으로 연결될 수 있음을 적절히 표현하고 있습니다.

domain/feed/src/main/java/com/into/websoso/feed/model/Feeds.kt (1)

1-7: LGTM!

Feeds 컬렉션 모델이 카테고리 컨텍스트와 페이지네이션 상태(isLoadable)를 명확하게 표현하고 있습니다.

domain/feed/src/main/java/com/into/websoso/feed/mapper/FeedMapper.kt (1)

8-41: 매핑 로직이 정확합니다.

데이터 계층의 Entity를 도메인 모델로 변환하는 매핑 함수들이 모든 속성과 중첩 객체(User, Novel)를 정확하게 매핑하고 있습니다.

constructor(
private val feedRepository: FeedRepository,
) {
private var previousCategory: String = ""
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

동시성 문제: previousCategory 상태의 스레드 안전성 보장 필요

previousCategory가 가변 상태로 관리되고 있어, UseCase가 싱글톤으로 주입될 경우(일반적인 Hilt/Dagger 동작) 여러 코루틴에서 동시에 다른 카테고리를 조회할 때 race condition이 발생할 수 있습니다.

문제 시나리오:

  1. 코루틴 A가 "로맨스" 카테고리 조회 시작
  2. 코루틴 B가 "판타지" 카테고리 조회 시작
  3. 두 코루틴이 previousCategory를 동시에 읽고 쓰면서 캐시 클리어 로직이 잘못 실행되거나 previousCategory 값이 손상될 수 있음

해결 방안:

  • 방안 1 (권장): previousCategory를 제거하고 repository에서 현재 카테고리 상태 관리
  • 방안 2: @Synchronized 또는 Mutex를 사용한 동기화 추가
  • 방안 3: 카테고리별로 UseCase 인스턴스를 스코핑
🔒 방안 1을 적용한 수정 제안 (stateless UseCase)
 class GetFeedsUseCase
     @Inject
     constructor(
         private val feedRepository: FeedRepository,
     ) {
-        private var previousCategory: String = ""
-
         suspend operator fun invoke(
             selectedCategory: String,
             lastFeedId: Long = INITIAL_ID,
         ): Feeds {
             val isFeedRefreshed: Boolean = lastFeedId == INITIAL_ID
-            val isCategorySwitched: Boolean = previousCategory != selectedCategory
+            val isCategorySwitched: Boolean = feedRepository.currentCategory != selectedCategory
             if ((isFeedRefreshed || isCategorySwitched) && feedRepository.cachedFeeds.isNotEmpty()) {
                 feedRepository.clearCachedFeeds()
             }
 
             return feedRepository
                 .fetchFeeds(
                     category = selectedCategory,
                     lastFeedId = lastFeedId,
                     size = if (isFeedRefreshed or isCategorySwitched) INITIAL_REQUEST_SIZE else ADDITIONAL_REQUEST_SIZE,
                 ).toDomain()
-                .also { previousCategory = selectedCategory }
         }
 
         companion object {
             private const val INITIAL_ID: Long = 0
             private const val INITIAL_REQUEST_SIZE = 20
             private const val ADDITIONAL_REQUEST_SIZE = 10
         }
     }

Repository에 currentCategory 관리 추가:

// FeedRepository에 추가
var currentCategory: String = ""
    private set

fun fetchFeeds(...): FeedsEntity {
    // fetch logic
    currentCategory = category
    // ...
}

Also applies to: 20-20, 31-31

🤖 Prompt for AI Agents
In @domain/feed/src/main/java/com/into/websoso/feed/GetFeedsUseCase.kt at line
13, The mutable field previousCategory in GetFeedsUseCase is not thread-safe and
can cause races when the UseCase is singleton; remove previousCategory from
GetFeedsUseCase and delegate category state to the repository (e.g., add a
currentCategory property and update it inside the repository's fetch method) so
GetFeedsUseCase becomes stateless, or if you must keep UseCase-owned state,
protect access to previousCategory using a Mutex or synchronize the
getters/setters (or scope UseCase per category) to prevent concurrent
reads/writes; update all references to previousCategory in GetFeedsUseCase and
related call sites to use the repository's currentCategory API or the new
synchronized accessors.

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.

feat: 피드 데이터 레이어 모듈 분리

3 participants