Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Aug 22, 2025

👻 PULL REQUEST

📄 작업 내용

  • CommentInfo, UserComment, ContentComment로 파편화된 엔티티를 CommentTemp로 변경했어요.
  • 사용하지 않는 Comment, Content 관련 엔티티를 삭제했어요.
  • CommentTemp, ContentTemp 엔티티명을 각각 Comment, Content로 변경했어요.

📚 참고자료

👀 리뷰어에게 전달할 사항

  • 이제 진짜 온보딩 리팩뿐이야!

✅ 이번 PR에서 이런 부분을 중점적으로 체크해주세요!

  • x

🔗 연결된 이슈

Summary by CodeRabbit

  • 리팩터

    • 게시물(Content)과 댓글(Comment) 도메인을 단일 모델로 통합하고 필드명/구조를 일관화했습니다.
    • 댓글의 중첩(대댓글) 구조를 보존하는 재귀 매핑으로 댓글 트리와 페이지네이션 안정성을 개선했습니다.
    • 좋아요 수·상태, 삭제·표시 상태 처리 방식을 통일했습니다.
  • UI

    • 홈 상세·프로필 화면 전반에서 게시물·댓글 표시 및 상호작용(좋아요, 신고, 차단, 고스트, 답글) 로직을 신규 모델에 맞게 정비해 일관성과 안정성을 향상했습니다.

@youz2me youz2me requested a review from JinUng41 August 22, 2025 07:58
@youz2me youz2me self-assigned this Aug 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

댓글 도메인 모델을 단일 Comment로 통합했고, 이에 따른 Mapper/Repository/UseCase/UI 전반의 반환 타입과 사용처를 Comment/Content 기반으로 변경했습니다. 댓글 매핑은 재귀적 children 처리와 contentID 인자를 사용하도록 조정되었습니다.

Changes

Cohort / File(s) Summary
Domain 엔티티 통합
Wable-iOS/Domain/Entity/Comment.swift, Wable-iOS/Domain/Entity/Content.swift
CommentInfo/UserComment/ContentComment 제거, 단일 Comment 도입. ContentTemp/ContentInfo/UserContent 제거, 단일 Content 도입 및 필드 평탄화.
Mapper 업데이트
Wable-iOS/Data/Mapper/CommentMapper.swift, Wable-iOS/Data/Mapper/ContentMapper.swift
사용자/콘텐츠 댓글 매퍼가 모두 [Comment] 반환으로 통일; FetchContentComments 매핑은 contentID 인자 수용 및 재귀적 children 매핑. ContentMapper 반환 타입을 Content로 변경.
Repository 인터페이스/구현
Wable-iOS/Domain/RepositoryInterface/CommentRepository.swift, Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift, Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift, Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift
Comment/Content로 반환 타입 전환(Combine/async variants 포함). Mock 데이터 구조와 인덱스 접근도 Comment/Content 형태로 갱신.
UseCase 계층
Wable-iOS/Domain/UseCase/* (Home/Profile)
모든 관련 UseCase 시그니처를 Content/Comment 기반으로 변경; 내부 로직은 동일하게 리포지토리 호출 위임.
DTO 정합성 조정
Wable-iOS/Infra/Network/DTO/Response/Comment/FetchContentComments.swift, .../FetchUserComments.swift
DTO 필드명 노출 변경: commentLikedNumberlikedCount (CodingKeys 매핑 유지).
Presentation (Home / Profile / Component Cells)
Wable-iOS/Presentation/* (여러 파일)
View/VM/Controller 전반에서 ContentTemp/ContentComment/UserCommentContent/Comment로 교체. 데이터소스, 셀 등록, 액션 핸들러, 좋아요/삭제/신고/프로필 네비게이션 참조 경로 갱신.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as 사용자
  participant VC as ViewController
  participant VM as ViewModel
  participant UC as UseCase
  participant Repo as Repository
  participant Net as Network
  participant Map as Mapper

  rect rgba(200,220,255,0.18)
  note right of VC: 댓글 목록 요청 흐름 (단일 Comment)
  VC->>VM: fetchContentCommentList(contentID, cursor)
  VM->>UC: execute(contentID, cursor)
  UC->>Repo: fetchContentCommentList(contentID, cursor)
  Repo->>Net: GET /contents/{id}/comments
  Net-->>Repo: DTO.FetchContentComments[]
  Repo->>Map: toDomain(contentID, DTO[])
  Map-->>Repo: [Comment] (children 재귀 포함)
  Repo-->>UC: [Comment]
  UC-->>VM: [Comment]
  VM-->>VC: [Comment] -> UI 갱신
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Assessment against linked issues

Objective Addressed Explanation
Comment 엔티티 통합 (#268)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Content 도메인 전면 교체 — Wable-iOS/Domain/Entity/Content.swift 이슈 #268은 댓글 엔티티 통합을 목표로 함. 콘텐츠 모델(flatten) 전면 교체는 명시된 목표에 포함되어 있지 않습니다.
ContentRepository 반환 타입 변경 — Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift 댓글 통합 이슈의 요구사항과 직접 연관되지 않음; 콘텐츠 리포지토리 시그니처 변경은 별도 범위입니다.
Home/Profile 뷰·뷰모델의 ContentTemp→Content 전파 — 예: Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift UI 레이어에서의 콘텐츠 모델 교체는 댓글 통합과 별개로 보이며, 이슈 범위를 벗어납니다.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#268-comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or `` to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@youz2me youz2me added ♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Aug 22, 2025
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: 3

Caution

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

⚠️ Outside diff range comments (14)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)

274-274: Optional 처리 개선 가능

createdDate가 nil일 경우 guard문으로 인해 메서드 전체가 종료됩니다. 이는 다른 UI 요소들도 설정되지 않는 문제를 야기할 수 있습니다.

다음과 같이 수정하는 것을 제안합니다:

-        guard let createdDate = info.createdDate else { return }
-        
-        infoView.configureView(
-            userProfileURL: info.author.profileURL,
-            userName: info.author.nickname,
-            userFanTeam: info.author.fanTeam,
-            opacity: info.opacity.value,
-            createdDate: createdDate,
-            postType: .content
-        )
+        if let createdDate = info.createdDate {
+            infoView.configureView(
+                userProfileURL: info.author.profileURL,
+                userName: info.author.nickname,
+                userFanTeam: info.author.fanTeam,
+                opacity: info.opacity.value,
+                createdDate: createdDate,
+                postType: .content
+            )
+        }
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (2)

214-219: 셀 보관 클로저에서 cell 강한 참조로 인한 순환 참조(메모리 누수) 위험

contentImageViewTapHandler 클로저가 cell을 강하게 캡처하고, 이 클로저가 셀에 저장되어 순환 참조가 발생합니다.

다음과 같이 cell을 약한 참조로 캡처하세요:

-                contentImageViewTapHandler: { [weak self] in
-                    guard let image = cell.contentImageView.image else { return }
+                contentImageViewTapHandler: { [weak self, weak cell] in
+                    guard let image = cell?.contentImageView.image else { return }
                     let photoDetailViewController = PhotoDetailViewController(image: image)
                     self?.navigationController?.pushViewController(photoDetailViewController, animated: true)
                 },

345-349: 당김새로고침 종료 타이밍 버그: isLoading == true에서 endRefreshing 호출

현재는 isLoading이 true로 바뀔 때 새로고침을 종료합니다. 로딩 종료 시점(false)에서 endRefreshing가 호출되어야 합니다.

다음과 같이 수정하세요:

-        viewModel.$isLoading
-            .receive(on: RunLoop.main)
-            .filter { $0 }
-            .sink { [weak self] _ in self?.collectionView.refreshControl?.endRefreshing() }
+        viewModel.$isLoading
+            .receive(on: RunLoop.main)
+            .sink { [weak self] isLoading in
+                if !isLoading { self?.collectionView.refreshControl?.endRefreshing() }
+            }
             .store(in: cancelBag)
Wable-iOS/Data/Mapper/ContentMapper.swift (3)

53-93: 목록 매핑에서도 동일한 초 포맷 오타 존재

배열 매핑 함수 역시 SS를 사용합니다. 아래와 같이 일괄 수정해 주세요.

-        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:SS"
+        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
  • contentImageURL가 빈 문자열일 경우 URL(string:)은 nil을 반환하므로 현재 구현은 안전합니다. 다만 서버가 상대경로를 반환하는 경우를 대비해 BaseURL 조합 유틸을 도입하면 재사용성이 좋아집니다.

95-135: 사용자 콘텐츠 매핑도 동일 이슈(SS → ss) 및 경미한 일관성 제안

  • 시간 포맷을 ss로 수정해야 합니다.
  • isDeletednil로 두는 선택은 엔드포인트 스키마 차이를 반영한 것으로 보입니다만, 동일 파일의 다른 함수들과 “데이터 부재 == nil”이라는 규칙을 주석으로 명시해 두면 가독성이 좋아집니다.
-        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:SS"
+        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

원하시면 세 엔드포인트에 대한 매핑 유닛 테스트(시간 파싱, status 결정, Optional 필드 처리)를 생성해 드리겠습니다.


13-51: DateFormatter 패턴 수정 필요: “SS” → “ss”

“yyyy-MM-dd HH:mm:SS”에서 대문자 SS는 밀리초(fractional seconds) 패턴으로, 서버에서 보내는 초(second) 값(ss)과 불일치해 DateFormatter.date(from:)가 nil을 반환하거나 잘못 파싱됩니다. 다음 세 곳 모두 일괄 변경이 필요합니다.

• Wable-iOS/Data/Mapper/ContentMapper.swift:15
• Wable-iOS/Data/Mapper/ContentMapper.swift:55
• Wable-iOS/Data/Mapper/ContentMapper.swift:97

각 라인 수정 예시:

-        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:SS"
+        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

추가로, 동일 파일 내에서 DateFormatter 인스턴스를 매번 새로 생성하므로 성능 최적화를 위해 공용 포맷터를 정의하는 것을 권장합니다:

private extension ContentMapper {
    static let kstFormatter: DateFormatter = {
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd HH:mm:ss"
        f.timeZone = TimeZone(abbreviation: "KST")
        return f
    }()
}

그 후 toDomain 함수 내에서는

let dateFormatter = Self.kstFormatter

로 교체해 주세요.

Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)

219-252: createdDate가 nil일 때 조기 반환으로 인해 UI 잔상/미설정 가능성

createdDate가 없으면 바로 return되어, replyButton/likeButton/ghostButton 등의 상태가 이전 셀 상태로 남을 수 있습니다(prepareForReuse 전환 타이밍에 따라 발생). 조기 반환을 피하고, 버튼 설정은 createdDate 여부와 무관하게 수행하도록 위치를 조정하는 편이 안전합니다.

아래와 같이 버튼 설정을 먼저 하고, createdDate는 if-let로 분기해주세요.

         contentLabel.attributedText = info.text.pretendardString(with: .body4)
-        
-        guard let createdDate = info.createdDate else { return }
-        infoView.configureView(
-            userProfileURL: info.author.profileURL,
-            userName: info.author.nickname,
-            userFanTeam: info.author.fanTeam,
-            opacity: info.opacity.value,
-            createdDate: createdDate,
-            postType: .comment
-        )
-        
-        replyButton.configureButton()
-        likeButton.configureButton(isLiked: info.isLiked, likeCount: info.likeCount, postType: .comment)
+        replyButton.configureButton()
+        likeButton.configureButton(isLiked: info.isLiked, likeCount: info.likeCount, postType: .comment)
+
+        if let createdDate = info.createdDate {
+            infoView.configureView(
+                userProfileURL: info.author.profileURL,
+                userName: info.author.nickname,
+                userFanTeam: info.author.fanTeam,
+                opacity: info.opacity.value,
+                createdDate: createdDate,
+                postType: .comment
+            )
+        } else {
+            // TODO: createdDate 미존재 시의 표시 전략(스켈레톤/플레이스홀더/숨김) 결정
+            infoView.configureView(
+                userProfileURL: info.author.profileURL,
+                userName: info.author.nickname,
+                userFanTeam: info.author.fanTeam,
+                opacity: info.opacity.value,
+                createdDate: Date(),
+                postType: .comment
+            )
+        }
Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (1)

167-179: 밴 트리거 타입 오표기(.content → .comment) 가능성

댓글 밴 API 호출에서 triggerType이 .content로 전달됩니다. 도메인 의미상 .comment가 맞을 가능성이 큽니다. 서버 스키마와 매칭되는지 확인 후 수정이 필요합니다.

-                try await reportRepository.createBan(
-                    memberID: userID,
-                    triggerType: .content,
-                    triggerID: commentID
-                )
+                try await reportRepository.createBan(
+                    memberID: userID,
+                    triggerType: .comment,
+                    triggerID: commentID
+                )

또한 동일 로직을 호출하는 VC(OtherProfileViewController)의 밴 액션은 comment.id를 넘기므로 타입만 바로잡으면 일관됩니다.

Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (1)

126-151: Mock 데이터의 isDeleted 기본값을 false로 변경해주세요
HomeDetailViewController.updateComments(_:)에서 옵셔널 언래핑(guard let isDeleted = comment.isDeleted)을 사용해 nil인 경우 항목을 제거하기 때문에, 목 데이터는 명시적으로 false를 지정해야 UI에 정상적으로 표시됩니다.

수정 위치

  • Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift

제안된 변경사항

-static let mockUserComments: [Comment] = {
+static let mockUserComments: [Comment] = {
     let mockContentID = -1
     let mockUser = User(
         id: 167,
         nickname: "MockUser",
         profileURL: URL(string: "https://fastly.picsum.photos/id/1010/30/30.jpg?hmac=X5ekkqmSMlhAupHWilf0AAhRhn2_j47ENiy_PH8aFGM"),
         fanTeam: .t1
     )
     
     let temp: [Comment] = (1...52).map { number in
         Comment(
             id: number,
             author: mockUser,
             text: "\(number)번째",
             contentID: mockContentID,
-            isDeleted: nil,
+            isDeleted: false,
             createdDate: .now,
             parentContentID: nil,
             children: [],
             likeCount: 0,
             isLiked: false,
             opacity: .init(value: 0),
             status: .normal
         )
     }
     return temp
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)

586-601: updateContent 전달 시 불필요한 재구성 및 isDeleted 강제 false 설정

서버에서 내려온 isDeleted 값을 무시하고 false로 고정하고 있습니다. 원본 content를 그대로 전달하세요.

- owner.updateContent(
-     Content(
-         id: content.id,
-         author: content.author,
-         text: content.text,
-         title: content.title,
-         imageURL: content.imageURL,
-         isDeleted: false,
-         createdDate: content.createdDate,
-         isLiked: content.isLiked,
-         likeCount: content.likeCount,
-         opacity: content.opacity,
-         commentCount: content.commentCount,
-         status: content.status
-     )
- )
+ owner.updateContent(content)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (4)

142-155: 컴파일 오류: catch 이후 replaceError는 사용할 수 없습니다

catch 이후 퍼블리셔의 Failure가 Never로 고정되므로 replaceError(with:)를 더 이상 호출할 수 없습니다. 또한 동일 오류 처리가 중복됩니다. 아래처럼 replaceError를 제거하고 필요 시 eraseToAnyPublisher()로 타입을 지우세요.

-                let contentPublisher = owner.fetchContentInfoUseCase.execute(contentID: owner.contentID)
-                    .map { content -> Content? in
-                        return content
-                    }
-                    .catch { error -> AnyPublisher<Content?, Never> in
+                let contentPublisher = owner.fetchContentInfoUseCase.execute(contentID: owner.contentID)
+                    .map { content -> Content? in content }
+                    .catch { error -> AnyPublisher<Content?, Never> in
                         WableLogger.log("\(error.localizedDescription)", for: .error)
                         if case WableError.notFoundContent = error {
                             contentNotFoundSubject.send()
                         }
                         return .just(nil)
-                    }
-                    .replaceError(with: nil)
+                    }
+                    .eraseToAnyPublisher()

166-171: 메모리 안전성: handleEvents에서 self 강한 캡처

withUnretained(self) 체인 밖의 클로저에서 self를 강하게 캡처합니다. 잠재적 순환 참조를 피하려면 약한 캡처를 사용하세요.

-            .handleEvents(receiveOutput: { result in
+            .handleEvents(receiveOutput: { [weak self] result in
+                guard let self = self else { return }
                 let (_, comments) = result
                 isLoadingSubject.send(false)
                 isLastViewSubject.send(comments.isEmpty || self.flattenComments(comments).count < IntegerLiterals.commentCountPerPage)
             })

217-285: 중복/복잡도: 트리 탐색 로직을 재귀 헬퍼로 단순화하세요

현재 2중 for-루프로 루트/1단계 자식만 처리합니다. 재귀 헬퍼를 사용하면 다단계 트리도 자연스럽게 지원하고 가독성/복잡도 모두 개선됩니다.

적용 제안:

-            .sink(receiveValue: { isLiked, commentInfo in
-                var updatedComments = commentsSubject.value
-
-                for i in 0..<updatedComments.count {
-                    if updatedComments[i].id == commentInfo.id {
-                        let originalComment = updatedComments[i]
-                        updatedComments[i] = Comment(
-                            id: originalComment.id,
-                            author: originalComment.author,
-                            text: originalComment.text,
-                            contentID: originalComment.contentID,
-                            isDeleted: originalComment.isDeleted,
-                            createdDate: originalComment.createdDate,
-                            parentContentID: originalComment.parentContentID,
-                            children: originalComment.children,
-                            likeCount: isLiked ? originalComment.likeCount + 1 : originalComment.likeCount - 1,
-                            isLiked: isLiked,
-                            opacity: originalComment.opacity,
-                            status: originalComment.status
-                        )
-                        commentsSubject.send(updatedComments)
-                        return
-                    }
-
-                    for j in 0..<updatedComments[i].children.count {
-                        if updatedComments[i].children[j].id == commentInfo.id {
-                            let originalChild = updatedComments[i].children[j]
-                            var updatedChilds = updatedComments[i].children
-
-                            let updatedChildInfo = Comment(
-                                id: originalChild.id,
-                                author: originalChild.author,
-                                text: originalChild.text,
-                                contentID: originalChild.contentID,
-                                isDeleted: originalChild.isDeleted,
-                                createdDate: originalChild.createdDate,
-                                parentContentID: originalChild.parentContentID,
-                                children: originalChild.children,
-                                likeCount: isLiked ? originalChild.likeCount + 1 : originalChild.likeCount - 1,
-                                isLiked: isLiked,
-                                opacity: originalChild.opacity,
-                                status: originalChild.status
-                            )
-
-                            updatedChilds[j] = updatedChildInfo
-
-                            updatedComments[i] = Comment(
-                                id: updatedComments[i].id,
-                                author: updatedComments[i].author,
-                                text: updatedComments[i].text,
-                                contentID: updatedComments[i].contentID,
-                                isDeleted: updatedComments[i].isDeleted,
-                                createdDate: updatedComments[i].createdDate,
-                                parentContentID: updatedComments[i].parentContentID,
-                                children: updatedChilds,
-                                likeCount: updatedComments[i].likeCount,
-                                isLiked: updatedComments[i].isLiked,
-                                opacity: updatedComments[i].opacity,
-                                status: updatedComments[i].status
-                            )
-
-                            commentsSubject.send(updatedComments)
-                            return
-                        }
-                    }
-                }
-            })
+            .sink(receiveValue: { isLiked, target in
+                let updated = updateCommentLikes(in: commentsSubject.value, targetID: target.id, isLiked: isLiked)
+                commentsSubject.send(updated)
+            })

추가(파일 외부 보조 코드):

private extension HomeDetailViewModel {
    func updateCommentLikes(in comments: [Comment], targetID: Int, isLiked: Bool) -> [Comment] {
        comments.map { c in
            if c.id == targetID {
                return Comment(
                    id: c.id,
                    author: c.author,
                    text: c.text,
                    contentID: c.contentID,
                    isDeleted: c.isDeleted,
                    createdDate: c.createdDate,
                    parentContentID: c.parentContentID,
                    children: c.children,
                    likeCount: max(0, isLiked ? c.likeCount + 1 : c.likeCount - 1),
                    isLiked: isLiked,
                    opacity: c.opacity,
                    status: c.status
                )
            }
            guard !c.children.isEmpty else { return c }
            let newChildren = updateCommentLikes(in: c.children, targetID: targetID, isLiked: isLiked)
            return Comment(
                id: c.id,
                author: c.author,
                text: c.text,
                contentID: c.contentID,
                isDeleted: c.isDeleted,
                createdDate: c.createdDate,
                parentContentID: c.parentContentID,
                children: newChildren,
                likeCount: c.likeCount,
                isLiked: c.isLiked,
                opacity: c.opacity,
                status: c.status
            )
        }
    }
}

395-408: weak-capture 의도 무력화: withUnretained(self) 내부에서 self 직접 참조

withUnretained(self)를 사용했지만 self.contentID를 직접 참조하여 강한 캡처가 다시 발생합니다. owner를 사용하도록 수정하세요.

-                if postType == .content {
-                    return owner.deleteContentUseCase.execute(contentID: self.contentID)
+                if postType == .content {
+                    return owner.deleteContentUseCase.execute(contentID: owner.contentID)
🧹 Nitpick comments (31)
Wable-iOS/Domain/Entity/Content.swift (1)

18-19: Optional 타입 사용 검토 필요

isDeletedcreatedDate가 Optional로 선언되어 있습니다. 이들 필드가 항상 존재해야 하는 경우라면 non-optional로 변경하는 것을 고려해보세요. 특히 createdDate는 게시물의 필수 정보일 가능성이 높습니다.

Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)

22-24: execute 반환 타입 전환은 일관적임. 추가로 유효하지 않은 contentID에 대한 빠른 실패 처리 권장

도메인 전반의 Content 통합 방향과 일치합니다. 다만 잘못된 식별자(<= 0) 입력을 조기에 차단하면 리포지토리 호출을 줄이고 오류 전파가 명확해집니다.

예시(오류 케이스는 실제 WableError 케이스로 교체하세요):

 extension FetchContentInfoUseCase {
-    func execute(contentID: Int) -> AnyPublisher<Content, WableError> {
-        return repository.fetchContentInfo(contentID: contentID)
-    }
+    func execute(contentID: Int) -> AnyPublisher<Content, WableError> {
+        guard contentID > .zero else {
+            // TODO: 실제 에러 케이스로 교체 (예: .invalidParameter 또는 .notFoundContent)
+            return Fail(error: WableError.invalidParameter).eraseToAnyPublisher()
+        }
+        return repository.fetchContentInfo(contentID: contentID)
+    }
 }
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (1)

22-24: cursor 유효성 검증으로 방어적 프로그래밍 보완 권장

리포지토리 위임 자체는 문제없습니다. 다만 음수 커서 등의 비정상 입력을 조기 차단하면 예외 흐름이 예측 가능해집니다.

예시(오류 케이스는 실제 WableError 케이스로 교체하세요):

 extension FetchContentListUseCase {
-    func execute(cursor: Int) -> AnyPublisher<[Content], WableError> {
-        return repository.fetchContentList(cursor: cursor)
-    }
+    func execute(cursor: Int) -> AnyPublisher<[Content], WableError> {
+        guard cursor >= .zero else {
+            return Fail(error: WableError.invalidParameter).eraseToAnyPublisher()
+        }
+        return repository.fetchContentList(cursor: cursor)
+    }
 }
Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)

23-25: Comment 통합 반영 OK. contentID/cursor 파라미터의 사전 검증 고려

반환 타입 전환은 일관적입니다. 동일하게 contentID(>0), cursor(≥0) 검증을 추가하면 네트워크 왕복을 줄일 수 있습니다.

예시(오류 케이스는 실제 WableError 케이스로 교체):

 extension FetchContentCommentListUseCase {
-    func execute(contentID: Int, cursor: Int) -> AnyPublisher<[Comment], WableError> {
-        return repository.fetchContentCommentList(contentID: contentID, cursor: cursor)
-    }
+    func execute(contentID: Int, cursor: Int) -> AnyPublisher<[Comment], WableError> {
+        guard contentID > .zero, cursor >= .zero else {
+            return Fail(error: WableError.invalidParameter).eraseToAnyPublisher()
+        }
+        return repository.fetchContentCommentList(contentID: contentID, cursor: cursor)
+    }
 }
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (3)

193-206: 미사용 클로저 파라미터 경고(SwiftLint) 정리

여러 Cell/Supplementary 등록 클로저에서 indexPath/elementKind를 사용하지 않습니다. _로 치환해 경고를 없애세요.

SwiftLint 경고 위치(예시): 237행. 아래와 같이 변경:

-        let profileInfoCellRegistration = CellRegistration<ProfileInfoCell, UserProfile> { cell, indexPath, item in
+        let profileInfoCellRegistration = CellRegistration<ProfileInfoCell, UserProfile> { cell, _, item in
             ...
         }
 
-        let contentCellRegistration = CellRegistration<ContentCollectionViewCell, Content> {
-            cell, indexPath, item in
+        let contentCellRegistration = CellRegistration<ContentCollectionViewCell, Content> {
+            cell, _, item in
             ...
         }
 
-        let commentCellRegistration = CellRegistration<CommentCollectionViewCell, Comment> {
-            cell, indexPath, item in
+        let commentCellRegistration = CellRegistration<CommentCollectionViewCell, Comment> {
+            cell, _, item in
             ...
         }
 
-        ) { supplementaryView, elementKind, indexPath in
+        ) { supplementaryView, _, _ in
             supplementaryView.onSegmentIndexChanged = { [weak self] in self?.viewModel.selectedIndexDidChange($0) }
         }

Also applies to: 208-234, 236-259, 260-265


147-149: 무의미해진 willDisplaySubject 파이프라인 연결 또는 제거

willDisplaySubject는 sink만 존재하고 실제로 send가 호출되지 않습니다. 디바운스 기반 무한 스크롤을 원하신다면 willDisplay에서 subject로 이벤트를 전달하세요. 아니면 subject 파이프라인을 제거하세요(권장: 한 경로만 유지).

디바운스 사용을 유지하는 경우:

-        if indexPath.item >= itemCount - 2 {
-            viewModel.willDisplayLast()
-        }
+        if indexPath.item >= itemCount - 2 {
+            willDisplaySubject.send(())
+        }

파이프라인은 그대로 사용됩니다(328-332행). 중복 호출을 피하려면 위 변경과 함께 147-149행의 직접 호출을 제거해야 합니다.

Also applies to: 328-332


160-161: 문구 일관성(UX 카피) 제안

초기 타이틀 기본값 "알 수 없음"과 바인딩 시 "알 수 없는 유저"가 혼재합니다. 단일 문구로 통일하면 UX 일관성이 좋아집니다.

예: 둘 다 "알 수 없는 유저"로 통일.

Also applies to: 336-338

Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1)

2-2: 파일 헤더 주석의 파일명 불일치

헤더 주석이 FetchUserContentUseCase.swift로 표기되어 있습니다. 실제 파일명/타입(FetchUserContentListUseCase)과 일치하도록 정정해 주세요.

-//  FetchUserContentUseCase.swift
+//  FetchUserContentListUseCase.swift
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (2)

254-267: contentLabel 설정 중복 제거

contentLabel.attributedText가 configureCell와 configureCommentType에서 중복 설정됩니다. 중복은 유지보수 비용을 높이고 스타일 불일치를 유발합니다. 한 곳으로 모아주세요.

-    func configureCommentType(info: Comment, commentType: CommentType) {
-        contentLabel.attributedText = info.text.pretendardString(with: .body4)
+    func configureCommentType(info: Comment, commentType: CommentType) {

Also applies to: 238-239


269-295: blind 상태 처리: 메인 큐 디스패치 불필요 + 제약은 remakeConstraints 권장

  • UI 스레드에서 호출되는 컨텍스트이므로 DispatchQueue.main.async는 불필요합니다.
  • blindImageView 제약은 상태 전환 시 idempotent 하도록 remakeConstraints를 권장합니다(중복 제약 축적 방지).
-            blindImageView.snp.makeConstraints {
+            blindImageView.snp.remakeConstraints {
                 $0.top.equalTo(infoView.snp.bottom).offset(12)
                 $0.leading.equalTo(likeButton)
                 $0.trailing.equalToSuperview().inset(16)
                 $0.bottom.lessThanOrEqualTo(ghostButton.snp.top).offset(-12)
                 $0.adjustedHeightEqualTo(50).priority(.high)
             }
-            
-            DispatchQueue.main.async {
-                self.contentLabel.isHidden = true
-                self.blindImageView.isHidden = false
-            }
+            contentLabel.isHidden = true
+            blindImageView.isHidden = false
Wable-iOS/Domain/Entity/Comment.swift (2)

12-26: parentContentID 의미 재검증 필요(명칭/스키마 정합성)

Comment의 부모를 가리키는 식별자가 콘텐츠인지, 부모 댓글인지 명확하지 않습니다. 부모 댓글을 의미한다면 parentCommentID가 더 정확합니다. 매퍼/레포지토리/백엔드 스키마와 의미가 일치하는지 확인 부탁드립니다.

명확히 부모 댓글을 지칭한다면 필드명을 parentCommentID로, 콘텐츠 ID는 contentID로 유지하는 것을 권장합니다.


12-26: 동시성 안전성 및 기본값 제안

  • isDeleted가 서버에서 누락될 수 있다면 Bool? 보다는 기본값 false가 도메인 사용성에 유리합니다.
  • Comment가 여러 계층에서 전달/보관된다면 Sendable 채택 검토를 권장합니다(필드가 모두 Sendable일 경우).
-    let isDeleted: Bool?
+    let isDeleted: Bool // 기본값 false를 매퍼에서 부여

추가로 extension Comment: @unchecked Sendable {} 또는 필드 Sendable 충족 시 struct Comment: Sendable 검토.

Wable-iOS/Presentation/Profile/Other/View/OtherProfileViewController.swift (2)

207-258: 클로저의 미사용 파라미터 경고(SwiftLint) 정리

content 셀 registration 클로저에서 indexPath를 사용하지 않습니다. _로 치환하여 경고를 제거하세요.

-        let contentCellRegistration = CellRegistration<ContentCollectionViewCell, Content> {
-            cell, indexPath, item in
+        let contentCellRegistration = CellRegistration<ContentCollectionViewCell, Content> {
+            cell, _, item in

260-307: 클로저의 미사용 파라미터 경고(SwiftLint) 정리 + 액션 핸들러 일관성 유지

  • comment 셀 registration에서도 indexPath 미사용이므로 _ 치환이 필요합니다.
  • 액션 핸들러는 [weak self]로 일관되게 캡처되어 있어 메모리 안전성 면에서 좋습니다.
-        let commentCellRegistration = CellRegistration<CommentCollectionViewCell, Comment> {
-            cell, indexPath, item in
+        let commentCellRegistration = CellRegistration<CommentCollectionViewCell, Comment> {
+            cell, _, item in
Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (3)

82-92: Void 함수 호출에 삼항 연산자 사용 지양(SwiftLint) — 가독성 개선

삼항으로 부수효과만 있는 호출을 분기하는 패턴은 경고를 유발하고 가독성이 떨어집니다. if/else로 치환해 주세요.

-                isLiked
-                ? try await contentLikedRepository.deleteContentLiked(contentID: contentID)
-                : try await contentLikedRepository.createContentLiked(
-                    contentID: contentID,
-                    triggerType: TriggerType.Like.contentLike.rawValue
-                )
+                if isLiked {
+                    try await contentLikedRepository.deleteContentLiked(contentID: contentID)
+                } else {
+                    try await contentLikedRepository.createContentLiked(
+                        contentID: contentID,
+                        triggerType: TriggerType.Like.contentLike.rawValue
+                    )
+                }

106-118: Void 함수 호출에 삼항 연산자 사용 지양(SwiftLint) — 가독성 개선

toggleLikeComment도 동일한 패턴입니다.

-                isLiked
-                ? try await commentLikedRepository.deleteCommentLiked(commentID: commentID)
-                : try await commentLikedRepository.createCommentLiked(
-                    commentID: commentID,
-                    triggerType: TriggerType.Like.commentLike.rawValue,
-                    notificationText: item.commentList[index].text
-                )
+                if isLiked {
+                    try await commentLikedRepository.deleteCommentLiked(commentID: commentID)
+                } else {
+                    try await commentLikedRepository.createCommentLiked(
+                        commentID: commentID,
+                        triggerType: TriggerType.Like.commentLike.rawValue,
+                        notificationText: item.commentList[index].text
+                    )
+                }

171-179: isGhostCompleted 플래그 용도 재검토

banComment 성공 시 isGhostCompleted = true를 설정합니다. 네이밍 상 ‘고스트 처리 완료’로 읽히므로 ‘밴 완료’와 혼선이 발생할 수 있습니다. UI 바인딩도 현재 isGhostCompleted를 구독하지 않는 것으로 보입니다(VC 파일 참조). 의도 확인 후 분리된 상태 플래그 사용 또는 네이밍 변경을 권장합니다.

Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (3)

170-183: 좋아요 토글 시 likeCount 음수 방지 및 중복 객체 재생성 최소화 제안

  • 현재 isLiked ? +1 : -1 로직은 최소값 제한이 없어 0 미만으로 내려갈 수 있습니다.
  • 동일 필드를 모두 나열하는 새 Content 생성이 반복됩니다. 도메인에 이미 like()/unlike()가 있다면(프로필 VM에서 사용) 동일 접근을 권장합니다.

다음과 같이 최소한의 수정으로 안전하게 처리할 수 있습니다.

-    likeCount: isLiked ? originalContent.likeCount + 1 : originalContent.likeCount - 1,
+    likeCount: max(0, isLiked ? originalContent.likeCount + 1 : originalContent.likeCount - 1),

또는 도메인 메서드를 활용해 재생성을 줄입니다:

- let updatedContent = Content(
-   id: originalContent.id,
-   ...
-   isLiked: isLiked,
-   likeCount: max(0, isLiked ? originalContent.likeCount + 1 : originalContent.likeCount - 1),
-   ...
- )
+ var updatedContent = originalContent
+ isLiked ? updatedContent.like() : updatedContent.unlike()

205-218: GHOST/BLIND 처리 코드 중복 — 헬퍼로 통합 권장

두 곳에서 동일한 Content 재구성 코드가 상태만 달리 반복됩니다. 유지보수를 위해 헬퍼를 추출하세요.

예시:

- let updatedContent = Content(
-   id: content.id,
-   author: content.author,
-   text: content.text,
-   title: content.title,
-   imageURL: content.imageURL,
-   isDeleted: content.isDeleted,
-   createdDate: content.createdDate,
-   isLiked: content.isLiked,
-   likeCount: content.likeCount,
-   opacity: opacity,
-   commentCount: content.commentCount,
-   status: .ghost
- )
+ let updatedContent = content
+   .withStatus(.ghost)
+   .withOpacity(opacity)

헬퍼는 별도 익스텐션으로 제공합니다(파일 외부에 추가):

extension Content {
  func withStatus(_ status: PostStatus) -> Content {
    Content(id: id, author: author, text: text, title: title, imageURL: imageURL,
            isDeleted: isDeleted, createdDate: createdDate, isLiked: isLiked,
            likeCount: likeCount, opacity: opacity, commentCount: commentCount, status: status)
  }
  func withOpacity(_ opacity: Opacity) -> Content {
    Content(id: id, author: author, text: text, title: title, imageURL: imageURL,
            isDeleted: isDeleted, createdDate: createdDate, isLiked: isLiked,
            likeCount: likeCount, opacity: opacity, commentCount: commentCount, status: status)
  }
}

Also applies to: 275-288


270-292: 들여쓰기 및 코드 스타일 불일치

해당 구간은 다른 블록 대비 들여쓰기가 깨져 있어 가독성이 떨어집니다. 아래와 같이 정렬을 권장합니다.

-    for i in 0..<updatedContents.count {
-        if updatedContents[i].author.id == userID {
-            let content = updatedContents[i]
-            let opacity = content.opacity.reduced()
-            
-            let updatedContent = Content(
+                for i in 0..<updatedContents.count {
+                    if updatedContents[i].author.id == userID {
+                        let content = updatedContents[i]
+                        let opacity = content.opacity.reduced()
+
+                        let updatedContent = Content(
                 id: content.id,
                 author: content.author,
                 text: content.text,
                 title: content.title,
                 imageURL: content.imageURL,
                 isDeleted: content.isDeleted,
                 createdDate: content.createdDate,
                 isLiked: content.isLiked,
                 likeCount: content.likeCount,
                 opacity: opacity,
                 commentCount: content.commentCount,
                 status: .blind
               )
-            
-            updatedContents[i] = updatedContent
-        }
-    }
+                        updatedContents[i] = updatedContent
+                    }
+                }
Wable-iOS/Data/Mapper/CommentMapper.swift (1)

72-90: 재귀 매핑 성능/스택 안정성 검토 제안

깊은 트리에서도 안전하나, 평균/최대 뎁스를 파악해 꼬리재귀 제거 또는 반복(flatten) 기반 변환 유틸을 준비해두면 디버깅 및 성능에 유리합니다.

Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (3)

120-123: SwiftLint: 삼항 연산자를 통한 Void 함수 호출 회피

경고(void_function_in_ternary)를 없애고 가독성을 높이기 위해 if문으로 교체를 권장합니다.

- var content = item.contentList[index]
- isLiked ? content.unlike() : content.like()
- item.contentList[index] = content
+ var content = item.contentList[index]
+ if isLiked {
+   content.unlike()
+ } else {
+   content.like()
+ }
+ item.contentList[index] = content

131-149: SwiftLint: 동일 이슈(댓글 좋아요 토글) 정리

위와 동일한 패턴으로 if문 전환을 권장합니다.

- var commentInfo = comment
- isLiked ? commentInfo.unlike() : commentInfo.like()
- item.commentList[index] = commentInfo
+ var commentInfo = comment
+ if isLiked {
+   commentInfo.unlike()
+ } else {
+   commentInfo.like()
+ }
+ item.commentList[index] = commentInfo

198-206: @published(UI 바인딩) 플래그 업데이트는 MainActor에서 일관 처리 권장

isLoadingMore = true 등 일부 플래그는 Task 외부/백그라운드에서 갱신될 수 있습니다. UI 일관성을 위해 MainActor에서 갱신하세요.

- isLoadingMore = true
+ await MainActor.run { isLoadingMore = true }

두 메서드(fetchMoreContentList, fetchMoreCommentList) 모두에 동일 적용을 권장합니다.

Also applies to: 224-234

Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2)

203-211: SwiftLint: 사용하지 않는 클로저 매개변수는 _로 치환

indexPath를 사용하지 않습니다. 경고 제거 및 가독성 향상을 위해 _로 변경하세요.

- [weak self] cell,
- indexPath,
- item in
+ [weak self] cell,
+ _,
+ item in

Also applies to: 356-362


226-231: Amplitude 이벤트 태그 오기 가능성(사소)

컨텐츠 좋아요인데 .clickLikeComment 이벤트를 로깅합니다. 분석 대시보드 일관성을 위해 컨텐츠용 태그 사용 여부 확인이 필요합니다.

Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (5)

189-203: 좋아요 수 음수 방지 및 중복 필드 복사 개선

좋아요 해제 시 likeCount가 0 이하가 될 수 있습니다. 0 미만으로 내려가지 않도록 클램핑하세요. 또한 필드 전부를 수동 복사하는 패턴이 반복됩니다.

-                    likeCount: isLiked ? content.likeCount + 1 : content.likeCount - 1,
+                    likeCount: max(0, isLiked ? content.likeCount + 1 : content.likeCount - 1),

추가로, 추후 편의를 위해 Content에 copy-with 헬퍼를 제공하는 것을 권장합니다.

예시(파일 외부 추가 코드):

extension Content {
    func copying(
        isLiked: Bool? = nil,
        likeCount: Int? = nil,
        status: Status? = nil
    ) -> Content {
        .init(
            id: id, author: author, text: text, title: title, imageURL: imageURL,
            isDeleted: isDeleted, createdDate: createdDate,
            isLiked: isLiked ?? self.isLiked,
            likeCount: likeCount ?? self.likeCount,
            opacity: opacity, commentCount: commentCount,
            status: status ?? self.status
        )
    }
}

246-247: 오탈자/네이밍: updatedChildsupdatedChildren

영문 복수형 표기가 어색합니다. 현재 접근을 유지한다면 변수명을 updatedChildren으로 정리하세요.

Also applies to: 263-263, 273-273


650-659: 재귀 평탄화로 일반화

현재는 1단계 자식까지만 평탄화합니다. 재귀 DFS로 일반화하면 트리 깊이에 독립적입니다.

-    func flattenComments(_ comments: [Comment]) -> [Comment] {
-        var flattenedComments: [Comment] = []
-        
-        for comment in comments {
-            flattenedComments.append(comment)
-            flattenedComments.append(contentsOf: comment.children)
-        }
-        
-        return flattenedComments
-    }
+    func flattenComments(_ comments: [Comment]) -> [Comment] {
+        var result: [Comment] = []
+        func dfs(_ node: Comment) {
+            result.append(node)
+            node.children.forEach(dfs)
+        }
+        comments.forEach(dfs)
+        return result
+    }

512-556: 중복 제거: 공통 변환 빌더 도입 제안 (Ghost)

Comment를 매번 전 필드 복사해 만드는 패턴이 반복됩니다. 트리 변환용 일반 함수를 도입하면 updateGhostComments, updateBannedComments, updateDeleteComments 모두를 단일 로직으로 통합할 수 있습니다. 유지보수/버그율 감소 효과가 큽니다. 원하면 변환 클로저 기반의 transformComments(_:rule:) 템플릿을 제안하겠습니다.


558-602: 중복 제거: 공통 변환 빌더 도입 제안 (Blind)

위와 동일 맥락입니다. 동일 패턴이 반복되니 공통화 고려 바랍니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d93318e and 96353cf.

📒 Files selected for processing (26)
  • Wable-iOS/Data/Mapper/CommentMapper.swift (3 hunks)
  • Wable-iOS/Data/Mapper/ContentMapper.swift (6 hunks)
  • Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (6 hunks)
  • Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (4 hunks)
  • Wable-iOS/Domain/Entity/Comment.swift (1 hunks)
  • Wable-iOS/Domain/Entity/Content.swift (1 hunks)
  • Wable-iOS/Domain/RepositoryInterface/CommentRepository.swift (1 hunks)
  • Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1 hunks)
  • Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Comment/FetchContentComments.swift (2 hunks)
  • Wable-iOS/Infra/Network/DTO/Response/Comment/FetchUserComments.swift (2 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (12 hunks)
  • Wable-iOS/Presentation/Home/View/HomeViewController.swift (2 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (10 hunks)
  • Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (6 hunks)
  • Wable-iOS/Presentation/Profile/Model/ProfileViewItem.swift (1 hunks)
  • Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (3 hunks)
  • Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (6 hunks)
  • Wable-iOS/Presentation/Profile/Other/View/OtherProfileViewController.swift (5 hunks)
  • Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (5 hunks)
  • Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (3 hunks)
  • Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (4)
  • fetchContentInfo (57-64)
  • fetchContentList (66-73)
  • fetchUserContentList (75-82)
  • fetchUserContentList (84-94)
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (4)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Domain/UseCase/Home/DeleteContentUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Domain/UseCase/Home/CreateContentUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (4)
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (2)
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Domain/RepositoryInterface/CommentRepository.swift (1)
Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (6)
  • fetchUserCommentList (20-30)
  • fetchUserCommentList (32-42)
  • fetchUserCommentList (93-95)
  • fetchUserCommentList (97-108)
  • fetchContentCommentList (44-54)
  • fetchContentCommentList (110-112)
Wable-iOS/Data/Mapper/ContentMapper.swift (1)
Wable-iOS/Data/Mapper/CommentMapper.swift (2)
  • toDomain (13-52)
  • toDomain (54-92)
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (3)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)
  • configureCell (218-252)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
  • configureCell (257-341)
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1)
  • toggleLikeComment (130-154)
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (3)
Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Domain/UseCase/Profile/FetchUserProfileUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Presentation/Profile/Other/View/OtherProfileViewController.swift (4)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)
  • configureCell (218-252)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
  • configureCell (257-341)
Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (4)
  • toggleLikeComment (99-123)
  • checkUserRole (125-127)
  • banComment (167-183)
  • ghostComment (202-217)
Wable-iOS/Presentation/Helper/WableTextSheetShowable.swift (1)
  • showReportSheet (41-50)
Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)
Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (1)
  • execute (18-24)
Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (1)
  • transform (91-506)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)
Wable-iOS/Presentation/WableComponent/Button/LikeButton.swift (1)
  • configureButton (47-70)
Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (2)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (2)
  • fetchUserContentList (75-82)
  • fetchUserContentList (84-94)
Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (4)
  • fetchUserCommentList (20-30)
  • fetchUserCommentList (32-42)
  • fetchUserCommentList (93-95)
  • fetchUserCommentList (97-108)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)
  • configureCell (218-252)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
  • configureCell (257-341)
Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (1)
Wable-iOS/Data/Mapper/CommentMapper.swift (2)
  • toDomain (13-52)
  • toDomain (54-92)
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (2)
Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (2)
  • fetchUserContentList (75-82)
  • fetchUserContentList (84-94)
Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (4)
  • fetchUserCommentList (20-30)
  • fetchUserCommentList (32-42)
  • fetchUserCommentList (93-95)
  • fetchUserCommentList (97-108)
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (4)
Wable-iOS/Domain/UseCase/Home/FetchContentCommentListUseCase.swift (1)
  • execute (23-25)
Wable-iOS/Domain/UseCase/Home/FetchContentListUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/UseCase/Profile/FetchContentInfoUseCase.swift (1)
  • execute (22-24)
Wable-iOS/Domain/Entity/Opacity.swift (1)
  • reduced (72-78)
Wable-iOS/Data/Mapper/CommentMapper.swift (1)
Wable-iOS/Data/Mapper/ContentMapper.swift (3)
  • toDomain (13-51)
  • toDomain (53-93)
  • toDomain (95-135)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift

[Warning] 237-237: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/Other/View/OtherProfileViewController.swift

[Warning] 261-261: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift

[Warning] 90-90: Using ternary to call Void functions should be avoided

(void_function_in_ternary)


[Warning] 116-116: Using ternary to call Void functions should be avoided

(void_function_in_ternary)

Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift

[Warning] 360-360: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift

[Warning] 121-121: Using ternary to call Void functions should be avoided

(void_function_in_ternary)


[Warning] 147-147: Using ternary to call Void functions should be avoided

(void_function_in_ternary)

Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift

[Warning] 244-244: where clauses are preferred over a single if inside a for

(for_where)

🔇 Additional comments (30)
Wable-iOS/Infra/Network/DTO/Response/Comment/FetchContentComments.swift (1)

21-21: 프로퍼티명 변경이 일관성 있게 적용되었습니다

commentLikedNumberlikedCount로 변경한 것이 적절합니다. 더 간결하고 이해하기 쉬운 이름이며, FetchUserComments.swift에서도 동일하게 적용되어 일관성을 유지하고 있습니다.

Also applies to: 35-35

Wable-iOS/Infra/Network/DTO/Response/Comment/FetchUserComments.swift (1)

18-18: DTO 필드명 통일이 잘 되었습니다

FetchContentComments와 동일하게 likedCount로 변경하여 DTO 간 일관성을 확보했습니다. CodingKey를 통해 JSON 키와의 매핑도 올바르게 유지되고 있습니다.

Also applies to: 29-29

Wable-iOS/Domain/Entity/Content.swift (1)

12-26: Content 엔티티 통합이 성공적으로 완료되었습니다

여러 개로 분산되어 있던 Content 관련 엔티티들을 하나의 Content 구조체로 통합한 것이 아키텍처를 단순화하고 유지보수성을 향상시켰습니다. Identifiable, Hashable, Likable 프로토콜 준수도 적절합니다.

Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)

258-258: ContentTemp에서 Content로의 타입 변경이 올바르게 적용되었습니다

configureCell 메서드의 파라미터 타입이 통합된 Content 엔티티를 사용하도록 변경되어 PR의 목적에 부합합니다.

Wable-iOS/Presentation/Home/View/HomeViewController.swift (2)

22-24: 타입 별칭이 적절하게 업데이트되었습니다

ContentTemp에서 Content로의 변경이 DataSource와 Snapshot에도 일관되게 적용되어 타입 안정성이 유지됩니다.


176-176: 셀 등록 타입 변경이 올바르게 적용되었습니다

CellRegistration의 제네릭 타입이 Content로 변경되어 통합된 도메인 모델과 일치합니다.

Wable-iOS/Presentation/Profile/Model/ProfileViewItem.swift (1)

13-15: 도메인 타입 통합 반영 확인 (Content/UserComment → Content/Comment)

뷰모델·뷰 레이어의 사용처와 정합성이 좋아졌습니다. 기본값([]) 유지도 적절합니다.

Wable-iOS/Presentation/Profile/My/View/MyProfileViewController.swift (2)

26-29: Item(Hashable) 안정성 확인: Content/Comment가 Hashable인지 검증 필요

DiffableDataSource의 Item이 Hashable을 요구하므로, 연관값인 Content/Comment도 Hashable이어야 합니다. 자동 합성은 가능하지만, 큰 모델일 경우 해시 비용과 스냅샷 변동성이 커질 수 있습니다. 가능하면 “id 기반” 커스텀 해시/동등성 구현을 고려하세요.

추가 제안(선택): Item의 해시를 id로만 계산하도록 커스터마이징

extension MyProfileViewController.Item {
    func hash(into hasher: inout Hasher) {
        switch self {
        case .profile(let p): hasher.combine("profile"); hasher.combine(p.user.id)
        case .content(let c): hasher.combine("content"); hasher.combine(c.id)
        case .comment(let c): hasher.combine("comment"); hasher.combine(c.id)
        case .empty(let e):   hasher.combine("empty");   hasher.combine(e.segment); hasher.combine(e.nickname)
        }
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case let (.profile(a), .profile(b)): return a.user.id == b.user.id
        case let (.content(a), .content(b)): return a.id == b.id
        case let (.comment(a), .comment(b)): return a.id == b.id
        case let (.empty(a), .empty(b)):     return a.segment == b.segment && a.nickname == b.nickname
        default: return false
        }
    }
}

96-111: 댓글 탭에서 셀 선택 시 네비게이션 기대 동작 확인 필요

현재 선택 시 항상 HomeDetailViewController로 이동합니다. comment 탭에서의 선택 동작(원글 상세로 이동, 혹은 댓글 상세/대댓글 보기)이 기획과 일치하는지 확인 바랍니다. viewModel.didSelect(index:)가 세그먼트에 따라 contentID를 올바르게 산출하는지 점검해 주세요.

원하시면, 세그먼트별 분기(콘텐츠/댓글)를 명시하는 가드 로직 초안을 제공하겠습니다.

Wable-iOS/Domain/UseCase/Profile/FetchUserContentListUseCase.swift (2)

18-24: 입력 검증 및 위임 로직 적절

userID 유효성 검증 후 Repository로 위임하는 흐름이 명확합니다. cursor(=last contentID)는 음수 허용 케이스가 Mock에 존재하므로 추가 검증이 필요 없어 보입니다.


12-13: ContentTemp 참조 없음 확인
ContentTemp에 대한 전수 검색 결과, Swift 파일 및 전체 코드베이스에서 해당 타입이 사용되고 있지 않음을 확인했습니다. 반환 타입 통합 변경([ContentTemp] → [Content])이 올바르게 반영된 것으로 보이며, 빌드에도 문제가 없을 것으로 예상됩니다.

Wable-iOS/Domain/UseCase/Profile/FetchUserCommentListUseCase.swift (2)

18-24: 입력 검증 흐름 양호

음수/0 사용자 ID 차단 후 Repository 위임으로 단순·명확합니다. cursor는 음수 초기값 지원이 있어 보이므로 그대로 유지해도 무방합니다.


12-13: UserComment/ContentComment 사용 잔존 여부 없음 확인

  • UserCommentContentComment 검색 결과, 코드베이스에 더 이상 해당 타입 사용이 없음을 확인했습니다.
  • 인터페이스/구현/호출부 모두 반환 타입이 [Comment]로 일치함을 확인했으니, 최종적으로 빌드만 통과되는지 확인해주세요.
Wable-iOS/Domain/RepositoryInterface/CommentRepository.swift (1)

13-19: Repository 구현체 시그니처 일치 검증 완료

모든 CommentRepository 구현체(실제 구현 및 Mock)가 프로토콜의 최신 시그니처를 올바르게 준수하고 있습니다.

  • Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift
    fetchUserCommentList, fetchContentCommentList, deleteComment, createComment 각 메서드가 인터페이스 정의와 완벽하게 일치
    • Async/await 버전 (fetchUserCommentList, deleteComment) 및 Combine 버전 모두 구현
  • MockCommentRepository 역시 동일한 형태로 프로토콜을 준수

실사용 패턴에 따라 추가 비동기 파생 메서드 도입을 여전히 고려해 보시면 좋겠습니다.
예:

func fetchContentCommentList(contentID: Int, cursor: Int) async throws -> [Comment]
func createComment(contentID: Int, text: String, parentID: Int?, parentMemberID: Int?) async throws
Wable-iOS/Domain/RepositoryInterface/ContentRepository.swift (1)

16-20: 잔존 ContentTemp 사용부 없음 확인
rg -nP --type=swift -C1 '\bContentTemp\b' 실행 결과, 전체 Swift 파일에서 ContentTemp 참조가 전혀 발견되지 않아 전환이 완전하게 이루어진 것으로 확인되었습니다.

  • 스크립트: rg -nP --type=swift -C1 '\bContentTemp\b'
  • 결과: 매칭 없음

추가 검토나 수정 없이 코드 변경을 승인합니다.

Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)

250-252: 신규 도메인 필드 적용 적절

LikeButton에 Comment.isLiked/likeCount 적용으로 도메인 통합 방향과 일관됩니다. 별다른 이슈 없어 보입니다.

Wable-iOS/Data/RepositoryImpl/ContentRepositoryImpl.swift (1)

57-94: Content 반환 타입으로의 일관된 전환 확인

fetchContentInfo/fetchContentList/fetchUserContentList(동기·비동기) 모두 Content로 통일되었고, ContentMapper.toDomain 사용도 일관적입니다. 에러 매핑도 적절합니다.

Wable-iOS/Presentation/Profile/Other/ViewModel/OtherProfileViewModel.swift (2)

71-73: 페이징 커서: Comment.id 사용으로의 전환 적절

통합된 Comment 모델에서 id를 커서로 사용하는 변경이 자연스럽고, 레포지토리 목 구현과도 일치합니다.


114-118: Optimistic UI 업데이트 적용 적절

네트워크 성공 후 like/unlike를 반영하는 현재 전략은 일관되며, 도메인 객체의 mutating 메서드 사용도 적절합니다. 에러 시 롤백을 별도로 고려할 필요가 있다면, 실패 시 이전 상태 복원 로직을 추가하세요.

Also applies to: 90-92

Wable-iOS/Presentation/Home/ViewModel/HomeViewModel.swift (1)

60-69: 타입 통합 반영 문제없음

Output와 내부 Subject가 모두 [Content]/Content로 일관되게 변경되었습니다. 다운스트림 의존성 타입 정합성도 유지될 것으로 보입니다.

Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (2)

20-30: 도메인 통합에 맞춘 반환 타입 변경 적합

[UserComment]/[ContentComment][Comment] 통합이 리포지토리 시그니처와 매핑 모두에 일관되게 반영되어 있습니다.

Also applies to: 32-42


44-54: 컨텐츠 댓글 매핑 시 contentID 주입 방식 적절

CommentMapper.toDomain(contentID, $0)로 상위 컨텍스트를 전달하는 방식이 재귀 매핑과 잘 맞습니다.

Wable-iOS/Data/Mapper/CommentMapper.swift (1)

13-52: 사용자 댓글 매핑 일관성 양호

  • 타임존(KST) 지정, 필드 매핑, 상태 계산(.blind/.ghost/.normal)이 일관적입니다.
  • isDeleted = false, children = [] 기본값도 UI 로직과 잘 맞습니다.
Wable-iOS/Presentation/Profile/My/ViewModel/MyProfileViewModel.swift (1)

78-80: 페이지네이션 커서 변경 적합

댓글 탭에서 커서를 last?.id로 전환한 부분이 통합된 Comment 도메인과 정합합니다.

Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)

25-27: 아이템 케이스 도메인 교체 적합

.content(Content), .comment(Comment)로의 전환이 데이터소스/셀 등록과 자연스럽게 연결됩니다.

Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift (5)

66-66: 입력 이벤트 타입 변경 확인: (Bool, Comment) 의미를 명확히 합시다

Bool 값이 "토글 후 목표 상태(true=좋아요됨)"인지 "토글 전 현재 상태"인지 명확히 해주세요. 현재 구현은 서버 호출 성공/실패와 무관하게 해당 Bool을 그대로 UI에 반영하는(optimistic update) 흐름입니다. 의도된 UX라면 주석으로 남기고, 실패 시 롤백 전략이 필요 없다면 명시해 주세요.


80-83: 도메인 정렬: Output 타입 전환 적절

Content?, [Comment]로의 전환이 도메인 모델 통합 방향과 일치합니다. 구독 측의 타입도 함께 갱신되었는지(VC, Cell 바인딩 등)만 확인하면 됩니다.


210-216: 좋아요 토글의 실패 처리 정책 확인(Optimistic Update 여부)

서버 오류 시 .asDriver(onErrorJustReturn: info)로 동일 입력을 흘려보내므로 UI는 성공한 것처럼 갱신됩니다. 의도된 낙관적 업데이트라면 OK, 아니라면 오류 시 isLiked 롤백이나 토스트 노출 등 보완이 필요합니다.


471-478: 페이지네이션 커서 산정 확인 필요

lastItem = commentsSubject.value.last는 루트 댓글의 마지막을 기준으로 커서를 계산합니다. UI가 (루트+자식) 평탄화 리스트를 스크롤한다면 실제 마지막 보이는 아이템은 자식일 수 있습니다. 백엔드 커서 정책이 “루트 댓글 ID 기준”인지 확인해 주세요. 필요하면 아래처럼 루트 기준임을 명시하거나, 다른 정책이면 평탄화 마지막을 사용하세요.

예시(평탄화 마지막 사용):

let last = flattenComments(commentsSubject.value).last
let cursor = last?.id ?? IntegerLiterals.initialCursor

604-648: 삭제 처리 시 텍스트/자식 보존 정책 재확인

isDeleted: true로만 표시되고 텍스트/자식은 그대로 유지됩니다. UI에서 삭제 댓글의 텍스트를 숨기거나 대체 문구로 바꿔야 한다면 여기서 텍스트 마스킹 처리(예: “삭제된 댓글입니다”)까지 반영하는 것이 안전합니다. 정책 확인 부탁드립니다.

Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

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

와우! 정말 대단하시네요~

고생많으셨습니다.

@Team-Wable Team-Wable deleted a comment from coderabbitai bot Aug 22, 2025
@youz2me youz2me merged commit 3c6a2f5 into develop Aug 22, 2025
1 check was pending
@youz2me youz2me deleted the refactor/#268-comment branch August 22, 2025 09:12
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: 2

Caution

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

⚠️ Outside diff range comments (3)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (3)

372-436: 댓글 셀의 설정 시트에서도 동일한 중복 present 패턴

오너 분기에서 이미 present(viewController, ...)(라인 398)를 호출하고, 분기문 종료 후 다시 present(viewController, ...)(라인 435-436)를 호출합니다. 이중 표시 위험이 있어 제거가 필요합니다.

다음처럼 공용 present 호출을 제거하세요:

-                    self.present(viewController, animated: true)
+                    // 오너 분기에서만 present 호출

586-601: 홈 상세 뷷 업데이트 시 isDeleted 원본 값 유지하기

서버/도메인에서 내려온 isDeleted 값을 UI에 고스란히 반영하지 않고 false로 고정하면, 실제 삭제·블라인드 처리된 콘텐츠가 상세 화면에서 잘못 표시될 위험이 있습니다. 아래 위치에서 하드코딩된 falsecontent.isDeleted로 변경해주세요.

• 수정 대상

  • 파일: Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift
  • 라인: 593

• 제안하는 diff

-                        isDeleted: false,
+                        isDeleted: content.isDeleted,

Content.isDeleted는 옵셔널(Bool?) 타입으로, 상세 화면에서도 삭제 여부 필터링에 활용되고 있습니다. 이처럼 원본 값을 유지해야 삭제/블라인드 상태 불일치 문제를 예방할 수 있습니다.


232-300: 바텀시트 표시 로직 및 showBottomSheet 구현 불일치로 인한 잘못된 인스턴스 표시

showBottomSheetpresent 호출 없이 WableBottomSheetController 인스턴스 생성과 액션 추가만 수행
(Wable-iOS/Presentation/Helper/WableBottomSheetShowable.swift:18-26)
Admin/일반 사용자 분기에서 showBottomSheet(actions:)로 설정된 새 인스턴스는 표시되지 않고,
이후 분기문 밖의 self.present(viewController, …)로 액션이 없는 원본 인스턴스만 노출됨
오너 분기에서도 분기 내·외부에 이중 present(viewController, …) 호출
일반 사용자 분기에서 중첩된 viewController.dismiss 호출(2회)로 과도한 뷰 해제 로직

수정 방향 (하나 선택):

(A) showBottomSheet 확장에 present(wableBottomSheet, animated: true) 추가 → 모든 분기에서 self.showBottomSheet(actions:…)만 호출
(B) showBottomSheet 호출 제거 → 공통으로 let sheet = WableBottomSheetController(); sheet.addActions(…); present(sheet, animated: true) 로직 통합

추가로 중첩 dismiss는 한 번만 호출하도록 정리하세요.
예시 최소 수정안 (방안 B 적용):

@@ HomeDetailViewController.swift: 일반 사용자 분기 내부
-    self.showBottomSheet(actions: reportAction)
+    // showBottomSheet는 present를 호출하지 않으므로 사용하지 않습니다.
+    let sheet = WableBottomSheetController()
+    sheet.addActions(reportAction)
+    self.present(sheet, animated: true)

@@ HomeDetailViewController.swift: 중첩 dismiss 제거
-    viewController.dismiss(animated: true, completion: {
-        viewController.dismiss(animated: true, completion: {
-            self?.didReportTappedSubject.send((item.author.nickname, message ?? item.text))
-        })
-    })
+    viewController.dismiss(animated: true) {
+        self?.didReportTappedSubject.send((item.author.nickname, message ?? item.text))
+    }

@@ HomeDetailViewController.swift: 오너 분기 후 이중 present 제거
-    // NOTE: 오너 분기에서만 직접 present 했으므로 여기서는 호출하지 않습니다.
-    self.present(viewController, animated: true)
+    // 최상위에서 한 번만 present 호출
♻️ Duplicate comments (1)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (1)

365-368: 최상위/답글 판별에서 parentContentID의 nil 케이스를 포함해야 합니다

현재 item.parentContentID == -1만 확인하여 nil 최상위 댓글이 .reply로 오분류될 수 있습니다. 또한 activeUserID 접근은 self.를 붙여 일관성을 유지하는 편이 좋습니다.

다음과 같이 수정하세요:

-                commentType: item.parentContentID == -1 ? .ripple : .reply,
-                authorType: item.author.id == activeUserID ? .mine : .others,
+                commentType: (item.parentContentID ?? -1) == -1 ? .ripple : .reply,
+                authorType: item.author.id == self.activeUserID ? .mine : .others,

검증 스크립트(모델 확인):

#!/bin/bash
# Comment 모델에서 parentContentID의 타입(옵셔널 여부)과 children 정의를 확인합니다.
rg -n -C2 'struct\s+Comment\b|class\s+Comment\b' -g '*.swift'
rg -n 'parentContentID' -g '*.swift'
rg -n 'children' -g '*.swift'
🧹 Nitpick comments (10)
Wable-iOS/Data/Mapper/CommentMapper.swift (4)

23-30: PostStatus 판정 로직 중복 — 헬퍼로 추출해 일관성/유지보수성 개선

두 군데에서 거의 동일한 블라인드/고스트 판정이 중복됩니다. 헬퍼로 통일하면 로직 변동 시 한 곳만 수정하면 됩니다.

아래처럼 치환을 제안합니다.

-            let postStatus: PostStatus
-            if let isBlind = comment.isBlind, isBlind {
-                postStatus = .blind
-            } else if comment.isGhost {
-                postStatus = .ghost
-            } else {
-                postStatus = .normal
-            }
+            let postStatus = mapStatus(isBlind: comment.isBlind, isGhost: comment.isGhost)
-            let postStatus: PostStatus
-            if comment.isBlind {
-                postStatus = .blind
-            } else if comment.isGhost {
-                postStatus = .ghost
-            } else {
-                postStatus = .normal
-            }
+            let postStatus = mapStatus(isBlind: comment.isBlind, isGhost: comment.isGhost)

추가(파일 내 임의 위치, 예: extension 하단):

// 헬퍼
private static func mapStatus(isBlind: Bool?, isGhost: Bool) -> PostStatus {
    if let isBlind, isBlind { return .blind }
    if isGhost { return .ghost }
    return .normal
}

Also applies to: 63-70


14-16: DateFormatter 매번 생성 — 정적 캐시로 비용 절감 가능

핫패스에서 반복 생성 시 비용이 쌓일 수 있습니다. 설정이 동일하다면 캐시를 고려해 주세요.

예시:

private static let kstFormatter: DateFormatter = {
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd HH:mm:ss"
    df.locale = Locale(identifier: "en_US_POSIX")
    df.timeZone = TimeZone(identifier: "Asia/Seoul")
    return df
}()

// 사용
let date = Self.kstFormatter.date(from: comment.time)

Also applies to: 55-57


54-54: 오버로드/오해 방지용 라벨 제안

toDomain(_ contentID: Int, _ response: ...)는 호출부 가독성이 떨어집니다. 아래처럼 라벨을 붙이면 의미가 더 분명해집니다.

-static func toDomain(_ contentID: Int, _ response: [DTO.Response.FetchContentComments]) -> [Comment]
+static func toDomain(for contentID: Int, from response: [DTO.Response.FetchContentComments]) -> [Comment]

44-45: parentContentID 네이밍 및 Sentinel 처리 개선 제안

현황을 확인한 결과, Comment 도메인 모델의 parentContentID 프로퍼티는 실제로 부모 “댓글”의 ID를 저장하고 있습니다.
이름만 보면 “콘텐츠(ID)”로 오해할 여지가 크므로, 다음과 같이 리팩터를 권장드립니다.

– 주요 변경 지점
• Wable-iOS/Domain/Entity/Comment.swift
– 기존: let parentContentID: Int?
– 제안: let parentCommentID: Int? (부모 댓글 ID임을 명확히)
• Wable-iOS/Data/Mapper/CommentMapper.swift
– 초기 루트 매핑: parentContentID: -1parentCommentID: nil
– DTO 매핑: parentContentID: comment.parentCommentIDparentCommentID: comment.parentCommentID
• Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift 및
Wable-iOS/Presentation/Home/ViewModel/HomeDetailViewModel.swift 전반
item.parentContentID == -1 비교 로직 → item.parentCommentID == nil
– 그 외 parentContentID 참조부 전부 parentCommentID로 변경

– Sentinel 값(-1) 대신 Optional(nil) 활용
기존에 “루트 댓글”을 표시하기 위해 -1을 사용했으나, Optional 타입에서는 nil이 더 안전하고 직관적입니다.

– 검토 포인트

  1. 네이밍 변경이 대규모 리팩터를 수반하므로, 영향 범위(Mapper, View/VC, 테스트 등)를 사전에 분석
  2. 변경 후 기존 비교 로직(== nil)으로 전환되었는지 확인
  3. 테스트 케이스 및 API 스펙 문서 업데이트
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (6)

40-40: didCommentHeartTappedSubject에 전체 Comment 전달 대신 식별자만 전달 권장

Subject를 통해 전체 Comment를 전파하면 불필요한 복사가 생길 수 있습니다. 뷰모델에서 필요한 정보가 "좋아요 토글 여부 + 댓글 ID"라면 (Bool, Int)로 축소해 의존도를 낮추는 편이 좋습니다.

다음과 같이 변경을 제안드립니다:

- private let didCommentHeartTappedSubject = PassthroughSubject<(Bool, Comment), Never>()
+ private let didCommentHeartTappedSubject = PassthroughSubject<(Bool, Int), Never>()

그리고 전송부도 함께 변경:

- self.didCommentHeartTappedSubject.send((cell.likeButton.isLiked, item))
+ self.didCommentHeartTappedSubject.send((cell.likeButton.isLiked, item.id))

입력 바인딩(라인 545) 및 ViewModel Input 정의도 동일하게 업데이트되어야 합니다.


207-210: 클로저의 미사용 매개변수(indexPath) 정리로 SwiftLint 경고 제거

해당 클로저에서 indexPath를 사용하지 않습니다. 언더스코어로 치환해 lint 경고를 해소하세요.

-        > {
-            [weak self] cell,
-            indexPath,
-            item in
+        > {
+            [weak self] cell,
+            _,
+            item in

359-362: 클로저의 미사용 매개변수(indexPath) 정리 (두 번째 CellRegistration)

여기도 indexPath를 사용하지 않으므로 언더스코어로 치환하세요.

-        > {
-            [weak self] cell,
-            indexPath,
-            item in
+        > {
+            [weak self] cell,
+            _,
+            item in

216-333: 셀에 주입하는 다수 핸들러 클로저에서 self 강한 캡처 발생 가능

Cell이 핸들러 클로저를 보관하는 구조라면, 현재 스코프의 guard let self = self 이후 내부 클로저가 VC를 강하게 캡처하게 됩니다. 재사용/해제 타이밍에 따라 참조 사이클 위험이 있어, 내부 핸들러 단위로 [weak self] 캡처를 권장합니다.

예시로 일부만 제안합니다(동일 패턴을 다른 핸들러에도 적용):

-                likeButtonTapHandler: {
+                likeButtonTapHandler: { [weak self] in
+                    guard let self = self else { return }
                     AmplitudeManager.shared.trackEvent(tag: .clickLikeComment)
                     
                     self.didContentHeartTappedSubject.send(cell.likeButton.isLiked)
                 },
-                contentImageViewTapHandler: {
+                contentImageViewTapHandler: { [weak self] in
+                    guard let self = self else { return }
                     guard let image = cell.contentImageView.image else { return }
                     
                     let photoDetailViewController = PhotoDetailViewController(image: image)
                     self.navigationController?.pushViewController(photoDetailViewController, animated: true)
                 },

동일한 조정을 settingButtonTapHandler, profileImageViewTapHandler, ghostButtonTapHandler, 아래의 commentButton.addAction에도 적용해주세요.


334-347: UIAction 핸들러에서도 self 약한 캡처 권장

commentButton.addAction 내부에서 self를 강하게 캡처하고 있습니다. VC–CollectionView–Cell–Closure 간 강한 참조 고리를 예방하기 위해 [weak self] 적용을 권장합니다.

-            cell.commentButton.addAction(UIAction(handler: { _ in
+            cell.commentButton.addAction(UIAction(handler: { [weak self] _ in
+                guard let self = self else { return }
                 AmplitudeManager.shared.trackEvent(tag: .clickWriteComment)
                 
                 self.createCommentButton.isEnabled = false
                 self.didCommentTappedSubject.send()
                 
                 self.commentTextView.text = ""
                 self.updatePlaceholder(for: item.author.nickname, type: .ripple)

동일 패턴을 다른 UIAction 등록 지점에도 적용해 주세요.


708-710: 무한 스크롤 트리거 조건 완화로 초기 데이터가 적을 때 과도 호출 가능

indexPath.item >= sectionItemCount - 5는 아이템 수가 5 미만일 때 사실상 대부분의 셀에서 트리거됩니다. 의도라면 유지해도 되지만, 과호출을 줄이려면 하한을 두는 것도 방법입니다.

-            if indexPath.item >= sectionItemCount - 5 && sectionItemCount > 0 {
+            if indexPath.item >= max(0, sectionItemCount - 5) && sectionItemCount > 0 {
                 willDisplayLastItemSubject.send()
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 96353cf and dd5823e.

📒 Files selected for processing (3)
  • Wable-iOS/Data/Mapper/CommentMapper.swift (3 hunks)
  • Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift (6 hunks)
  • Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (12 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Wable-iOS/Data/RepositoryImpl/CommentRepositoryImpl.swift
🧰 Additional context used
🧬 Code graph analysis (2)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (2)
Wable-iOS/Presentation/WableComponent/Cell/CommentCollectionViewCell.swift (1)
  • configureCell (218-252)
Wable-iOS/Presentation/WableComponent/Cell/ContentCollectionViewCell.swift (1)
  • configureCell (257-341)
Wable-iOS/Data/Mapper/CommentMapper.swift (1)
Wable-iOS/Data/Mapper/ContentMapper.swift (3)
  • toDomain (13-51)
  • toDomain (53-93)
  • toDomain (95-135)
🪛 SwiftLint (0.57.0)
Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift

[Warning] 360-360: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

🔇 Additional comments (8)
Wable-iOS/Data/Mapper/CommentMapper.swift (2)

42-42: isDeleted 매핑 일관성 확인 필요

  • 사용자 댓글 경로: isDeleted: false 하드코딩
  • 콘텐츠 댓글 경로: isDeleted: comment.isDeleted

두 API의 스키마 차이 때문일 수 있으나, 도메인 필드가 Optional이라면 사용자 댓글 경로도 nil로 두어 “정보 없음”과 “삭제되지 않음(false)”을 구분하는 편이 명확합니다. UI/정렬/필터 로직에서 false를 진실값으로 가정하는지 확인 바랍니다.

Also applies to: 82-82


85-85: children 재귀 매핑 깔끔합니다

자식 댓글 재귀 매핑 구조가 단순하고 읽기 좋습니다. 사이클만 없는 스키마라면 문제 없겠습니다.

Wable-iOS/Presentation/Home/View/HomeDetailViewController.swift (6)

25-27: Item 케이스 타입 전환(Content/Comment) 적절합니다

Diffable DataSource의 식별자 모델을 도메인 엔티티로 통일한 방향이 명확하고, 이후 스위칭 로직에도 자연스럽게 반영되었습니다.


203-206: Content 셀 제네릭 타입 전환 확인

CellRegistration 제네릭을 Content로 일관화한 변경이 문제 없습니다.


356-358: Comment 셀 제네릭 타입 전환 확인

Comment 기반으로 통일한 변경이 정상입니다.


780-791: 댓글 필터링에서 isDeleted 옵셔널 처리 개선 확인

guard comment.isDeleted != true 및 자식 필터 !($0.isDeleted ?? false)nil을 표시 대상으로 포함하는 방향이 적절합니다. 이전 가드(guard let)로 인해 누락되던 댓글이 표시될 것입니다.


783-797: 대댓글 전개가 1레벨에 한정됨(재귀 미적용) — 요구사항 확인 필요

현재는 children 1레벨만 펼칩니다. 사양이 다중 레벨(대대댓글)을 요구한다면 재귀 전개가 필요합니다. 현 구조로 충분하다면 그대로 유지해도 됩니다.

재귀 전개가 필요할 경우, 다음과 같이 간단한 DFS 형태로 펼칠 수 있습니다(개념 예시):

func flatten(_ comment: Comment) -> [Item] {
    guard comment.isDeleted != true else { return [] }
    return [.comment(comment)] + comment.children
        .filter { !($0.isDeleted ?? false) }
        .flatMap { flatten($0) }
}

227-230: 이벤트 태그 확인 완료: .clickLikeComment는 댓글 좋아요에 적합함
HomeDetailViewController.swiftlikeButtonTapHandler는 댓글 셀에서 호출되며, AmplitudeManager에도 .clickLikeComment"click_like_comment"로 정의되어 있어 일관성이 유지됩니다. 이 부분은 수정이 필요하지 않습니다.

Comment on lines 15 to 16
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

고정 포맷 파싱에는 en_US_POSIX와 식별자 기반 타임존(Asia/Seoul) 사용 권장

약어("KST")는 환경별로 인식 실패 가능성이 있고, 고정 포맷에는 en_US_POSIX가 정석입니다. 아래처럼 교체 및 보강을 제안합니다.

         dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
-        dateFormatter.timeZone = TimeZone(abbreviation: "KST")
+        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+        dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
📝 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
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(abbreviation: "KST")
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
🤖 Prompt for AI Agents
In Wable-iOS/Data/Mapper/CommentMapper.swift around lines 15 to 16, the
DateFormatter currently uses the "KST" abbreviation which can be unreliable and
lacks the recommended fixed-format locale; update the formatter to set locale =
Locale(identifier: "en_US_POSIX") and timeZone = TimeZone(identifier:
"Asia/Seoul") (with an optional fallback to .current or GMT if the identifier
returns nil) while keeping the same dateFormat so fixed-format parsing is robust
and deterministic.

Comment on lines 55 to 57
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

타임존/로케일 누락으로 시간 불일치(+9h) 및 기기 로케일 의존 위험

두 번째 매핑 함수에서는 timeZone/locale 설정이 없어서 디바이스 기본값을 따릅니다. 첫 번째 함수(KST 지정)와 결과가 달라질 수 있고, Asia/Seoul 기준 서비스라면 최대 +9시간 오차가 발생합니다. 고정 포맷 파싱에서는 en_US_POSIX 로케일 지정도 필수에 가깝습니다.

즉시 아래처럼 보완해 주세요.

         dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+        dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
📝 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
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
🤖 Prompt for AI Agents
In Wable-iOS/Data/Mapper/CommentMapper.swift around lines 55 to 57, the
DateFormatter is missing explicit timezone and locale settings causing
device-dependent parsing and up to +9h discrepancies; set the formatter's
timeZone to Asia/Seoul (or TimeZone(secondsFromGMT: 9*3600)) and its locale to
en_US_POSIX so the fixed "yyyy-MM-dd HH:mm:ss" format parses consistently across
devices.

youz2me added a commit that referenced this pull request Oct 26, 2025
[Refactor] Comment 엔티티 통합 및 네이밍 변경
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ refactor 기존 코드를 리팩토링하거나 수정하는 등 사용 (생산적인 경우) 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] Comment 엔티티 통합

3 participants