Skip to content

[Feat-T3-195] 제보하기 화면 구현 - #65

Merged
taipaise merged 3 commits into
developfrom
feat/report
Nov 9, 2025
Merged

[Feat-T3-195] 제보하기 화면 구현#65
taipaise merged 3 commits into
developfrom
feat/report

Conversation

@taipaise

@taipaise taipaise commented Nov 9, 2025

Copy link
Copy Markdown
Collaborator

🌁 Background

  • 제보하기 화면 구현

📱 Screenshot

iPhone SE3 iPhone 13 mini iPhone 16 Pro

👩‍💻 Contents

  • 제보하기 화면 구현

📝 Review Note

개발적인 부분

  1. 이미지
  • 작년에도 함께 고민하던 부분이었던 것 같은데요, 요 이미지를 어떻게 처리해야할 지 굉장히 고민입니다. 일단은 메인메모리에서 이미지 데이터를 바이너리 형식으로 들고 있기는 한데요, 개선이 필요해 보입니다.

  • 현재는 아래와 같은 과정을 통해 이미지를 처리합니다.

    1. 사용자가 앨범에서 이미지 선택하면, viewModel에 이미지의 바이너리 데이터를 전달합니다.
    2. ViewModel은 바이너리 데이터를 CurrentValueSubject에 저장하고, 이 정보를 publish합니다.
    3. ViewController는 다시 이 정보를 받아서 datasourcesnapshotapply 합니다.
  • 막연하게 지금 ViewModel에서 Subject에서 한 번, ViewController에서 DataSource에서 한 번, 마지막으로 사진을 표시하는 Cell에서 UIImage 형태로 한 번, 같은 이미지의 바이너리 데이터를 총 세 번이나 메인메모리에서 들고 있지 않을까 했습니다.

  • 지피티에 따르면, Data는 Copy-on-Write라서 수정하지 않는 한 바이트 버퍼는 공유되기 때문에, 별도로 메모리를 몇 배로 먹지 않는다고 하긴하는데,, 이미지 처리 방법에 대해 좀 더 공부할 필요가 있어보입니다.

  • 고려했던 방법으로는 일단 이미지를 파일시스템에 저장해두고 (앱 샌드박스의 temp 공간)이미지를 표시하거나 전송할 때 파일 입출력을 통해 처리해볼까? 했는데 바람직한 방법이라는 확신이 들지 않습니다.

  1. SelectableItemTableView
  • 커스텀 바텀 시트에 표시되는 tableView가 약간 수정되었습니다. 이전에는 선택결과를 저장해두고 있다가, 이후에 bottomSheet를 다시 열면 해당 값을 보여주고 있습니다.
  • 다만 카메라/앨범 선택 시에는 이 체크표시를 숨길 필요가 있었습니다. 따라서 tableView에 선택한 cell을 표시할것인지 선택하는 markIsSelected라는 프로퍼티를 추가했습니다. 생성된 수정자는 아래와 같습니다
    init(items: [T], selectedItem: T? = nil, markIsSelected: Bool = true) {
        self.items = items.sorted(by:  { $0.id < $1.id })
        self.selectedItem = selectedItem
        self.markIsSelected = markIsSelected
        super.init(nibName: nil, bundle: nil)
    }
  • 다른 코드에 영향을 주지 않기 위해 부득이하게 기본값을 true로 초기화하도록 했습니다. markIsSelected이 false인 경우에, tableView를 reload하기 전에 selectedItem을 nil로 초기화 합니다.
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let selectedItem = items[indexPath.row]
        if self.selectedItem == selectedItem {
            self.selectedItem = nil
        } else {
            self.selectedItem = selectedItem
        }

        if !markIsSelected { self.selectedItem = nil }

        itemTableView.reloadData()
        dismiss(animated: true)
    }
  • 또한, selectedItem 속성감시자도 일부 수정되었습니다. markIsSelected가 nil 인 경우에, delegate를 통해 따로 이벤트를 전송하지 않도록 아래와 같이 수정하였습니다.
    private var selectedItem: T? {
        didSet {
            if !markIsSelected && selectedItem == nil { return }
            delegate?.selectableItemTableView(self, didSelectItem: selectedItem)
        }
    }

기획 적인 부분

  1. 위치 선택 문제
  • 현재는 현위치만 설정할 수 있습니다. 따로 위치를 골라 선택할 수 없습니다.
  • 저번주 금요일 회의 결과, 즉석에서 사진을 찍는 것 외에 앨범에서 사진을 추가할 수 있도록 했습니다. 이 경우 사용자가 제보위치가 아닌 곳에서 사진 제보를 할 확률이 굉장히 높다 생각합니다. 따라서 위치를 설정할 수 있는 UI가 필요하지 않을까~ 합니다.

개선 하면 좋을 점 (데스노트 추가하겠습니다!)

  • 현재는 앨범에서 사진을 선택하는 경우 한 번에 한 장 선택할 수 있습니다. 추후 로직 고도화를 통해 한 번에 최대 세 장까지 추가할 수 있게 수정하겠습니다.
  • 아직 디기개 게시판에서 답변받지 못한 내용입니다만, 제보 내용의 줄 수가 많아져도 입력받는 textView의 높이는 고정입니다. 최대 100자라서 그렇게까지 줄 수가 많지는 않겠지만, 유동적인 height 변화도 고려해보면 좋을 것 같습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 신고 작성 화면 추가: 카테고리 선택, 제목/내용 입력, 위치 설정, 제출
    • 사진 첨부 기능 추가: 카메라/라이브러리 선택, 최대 3장 관리, 썸네일 갤러리
  • 개선 사항

    • 새로운 UI 컴포넌트 추가(입력 뷰·카메라 버튼·위치 버튼·필수 표시 라벨·삭제 알림 등)
    • 아이콘(카메라·위치·삭제) 및 관련 리소스 추가
    • 카메라·사진 권한 설명 문구 추가 (권한 안내)

@taipaise
taipaise requested a review from choijungp November 9, 2025 11:27
@taipaise taipaise self-assigned this Nov 9, 2025
@coderabbitai

coderabbitai Bot commented Nov 9, 2025

Copy link
Copy Markdown

Walkthrough

사진 기반 신고 기능을 위해 도메인 엔티티(ReportType, RoutineCompletionEntity), 사진 모델(PhotoItem)과 뷰모델(ReportViewModel), 신고 화면(ReportViewController) 및 관련 UI 컴포넌트·아이콘 자산과 권한 설명이 추가되었습니다. DI에 ReportViewModel 등록이 포함됩니다.

Changes

코호트 / 파일(s) 변경 요약
도메인 엔티티
\Projects/Domain/Sources/Entity/Enum/ReportType.swift`, `Projects/Domain/Sources/Entity/RoutineCompletionEntity.swift``
ReportType(lamp, road, etc) 열거형 추가. RoutineCompletionEntity 구조체( performedDate, routineId, completeYn, historySeq, routineType ) 추가
리소스(이미지 자산)
\Projects/Presentation/Resources/Images.xcassets/Report/Contents.json`, `.../camera_icon.imageset/Contents.json`, `.../location_icon.imageset/Contents.json`, `.../round_delete_icon.imageset/Contents.json``
Report 자산 카탈로그 및 각 아이콘 imageset의 Contents.json 추가(1x/2x/3x 메타데이터)
아이콘 공개 API
\Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift``
roundDeleteIcon, cameraIcon, locationIcon 정적 UIImage 프로퍼티 추가
DI 등록
\Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift``
ReportViewModel DI 컨테이너 등록 추가
권한 설명(Info.plist)
\SupportingFiles/Info.plist``
NSPhotoLibraryUsageDescription, NSCameraUsageDescription 한글 설명 항목 추가
신고 모델 및 선택 항목 적응
\Projects/Presentation/Sources/Report/Model/PhotoItem.swift`, `.../ReportType+.swift`, `.../SelectPhotoType.swift``
PhotoItem(id: UUID, data: Data) 추가. ReportTypeSelectPhotoTypeSelectableItem 준수 확장( id, displayName, description ) 추가
신고 뷰모델 및 컨트롤러
\Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift`, `Projects/Presentation/Sources/Report/View/ReportViewController.swift``
ReportViewModel 추가(입력 액션, 출력 퍼블리셔, 사진 최대 3장 제약 및 예외 퍼블리싱, 사진 추가/삭제 로직). ReportViewController 추가(사진 피커/카메라 통합, 컬렉션뷰 스냅샷, 폼 UI, 델리게이트 처리)
신고 관련 UI 컴포넌트
\Projects/Presentation/Sources/Report/View/LocationButton.swift`, `.../ReportCameraButton.swift`, `.../ReportPhotoCollectionViewCell.swift`, `.../ReportTextView.swift``
위치 버튼, 카메라 버튼(이미지 수 표시), 사진 셀(삭제 버튼·델리게이트), 텍스트뷰(모드별 동작·플레이스홀더·델리게이트) 추가
공통 UI 컴포넌트 및 홈 변경
\Projects/Presentation/Sources/Common/Component/RequiredTitleLabel.swift`, `.../SelectableItemTableView.swift`, `Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift`, `Projects/Presentation/Sources/Home/View/Component/RoutineDeleteAlertView.swift``
RequiredTitleLabel 추가. SelectableItemTableViewmarkIsSelected 플래그 및 선택 해제 시 delegate 동작 변경. HomeViewModel.outputvarlet으로 변경. RoutineDeleteAlertView 및 델리게이트 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant VC as ReportViewController
    participant VM as ReportViewModel
    participant Picker as ImagePicker<br/>(UIImagePicker/PHPicker)
    participant UI as Components

    User->>VC: 신고 화면 진입
    VC->>VM: Output 구독

    rect rgb(220,240,255)
    User->>UI: 카테고리 선택
    VC->>VM: action(.selectCategory(type))
    VM->>VM: categorySubject 업데이트
    VM-->>VC: categoryPublisher 발행
    end

    rect rgb(240,220,255)
    User->>UI: 카메라/라이브러리 선택
    VC->>Picker: 카메라/피커 표시
    Picker-->>VC: 이미지 데이터 반환
    VC->>VM: action(.selectPhoto(photoData))
    VM->>VM: photos 최대 3장 검사 및 추가 / 예외 발행
    VM-->>VC: selectedPhotoPublisher 발행
    VC->>VC: 컬렉션뷰 스냅샷 업데이트
    end

    rect rgb(220,255,220)
    User->>UI: 제목/내용 입력
    VC->>VM: action(.inputTitle/.inputContent)
    VM-->>VC: title/content 퍼블리시
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 주의 깊게 검토할 파일/영역:
    • Projects/Presentation/Sources/Report/View/ReportViewController.swift (카메라/피커 델리게이트, 권한·비동기 흐름, 컬렉션뷰 스냅샷)
    • Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (사진 추가/삭제 로직, 예외 처리, 퍼블리셔 초기화)
    • Projects/Presentation/Sources/Report/View/ReportTextView.swift (모드별 레이아웃·플레이스홀더 동작)
    • Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (markIsSelected 동작 변경으로 인한 delegate 호환성)
    • Info.plist의 권한 설명 문구 및 카메라/포토 권한 요청 흐름

Poem

🐇 당근 주머니에 셔터 소리 담아,
가로등·도로·기타를 살며시 골라,
뷰모델은 사진 셋까지 세어주고,
컨트롤러는 아이콘과 권한을 속삭여,
신고는 가볍게 하늘로 폴짝 날아가네 📸✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 설명합니다. 제보하기 화면 구현이라는 핵심 기능을 간결하게 요약하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/report

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (9)
Projects/Presentation/Sources/Home/View/Component/RoutineDeleteAlertView.swift (1)

53-85: 버튼 구성 코드의 중복을 고려해 보세요.

두 버튼의 구성 코드가 매우 유사합니다. 헬퍼 메서드를 추출하여 중복을 줄이고 유지보수성을 개선할 수 있습니다.

예시 리팩토링:

private func createStyledButton(title: String, action: @escaping () -> Void) -> UIButton {
    let button = UIButton()
    var configuration = UIButton.Configuration.filled()
    configuration.baseBackgroundColor = .white
    configuration.background.cornerRadius = 8
    configuration.attributedTitle = AttributedString(
        title,
        attributes: .init([.font: BitnagilFont(style: .body2, weight: .medium).font]))
    configuration.baseForegroundColor = BitnagilColor.navy500
    configuration.background.strokeColor = BitnagilColor.navy500
    configuration.background.strokeWidth = 1
    button.configuration = configuration
    button.addAction(UIAction { [weak self] _ in
        guard let self else { return }
        action()
    }, for: .touchUpInside)
    return button
}

그런 다음 configureAttribute()에서 다음과 같이 사용:

deleteDailyRoutineButton = createStyledButton(title: "당일만 삭제") {
    self.delegate?.routineDeleteAlertViewDidTapDeleteDailyRoutine(self)
}

deleteAllRoutineButton = createStyledButton(title: "전체 루틴 삭제") {
    self.delegate?.routineDeleteAlertViewDidTapDeleteAllRoutine(self)
}
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1)

53-73: 구현이 올바르지만 매직 넘버 추출을 고려해보세요.

이미지 카운트 표시 로직이 정확하게 구현되어 있습니다. 다만 라인 54의 하드코딩된 "3"을 상수로 추출하면 향후 최대 사진 개수 변경 시 유지보수가 용이해집니다.

다음과 같이 리팩토링할 수 있습니다:

 final class ReportCameraButton: UIButton {
     private enum Layout {
         static let imageSize: CGFloat = 20
         static let imageTopSpacing: CGFloat = 15
+        static let maxImageCount: Int = 3
     }

     func configure(imageCount: Int) {
-        let text = "\(imageCount)/3"
+        let text = "\(imageCount)/\(Layout.maxImageCount)"
Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (1)

27-34: 다국어 지원을 위한 지역화 고려

description 프로퍼티에 한국어 문자열이 하드코딩되어 있습니다. 향후 다국어 지원을 위해 Localizable.strings를 활용한 지역화를 고려해보세요.

예시:

 var description: String {
     switch self {
     case .camera:
-        return "직접 촬영하기"
+        return NSLocalizedString("select_photo_camera", comment: "")
     case .library:
-        return "사진 라이브러리에서 선택"
+        return NSLocalizedString("select_photo_library", comment: "")
     }
 }
Projects/Presentation/Sources/Report/Model/PhotoItem.swift (1)

10-18: 메모리 최적화 고려 사항

PhotoItem의 구조가 명확하고 Hashable 준수로 컬렉션 관리가 용이합니다. PR 설명에서 이미 언급하신 것처럼, 이미지 데이터를 메모리에 여러 번 보관하는 문제를 인지하고 계시네요. 향후 임시 디스크 저장소나 URL 기반 참조로 전환하면 메모리 사용량을 크게 개선할 수 있습니다.

메모리 최적화 구현을 위한 구체적인 설계나 코드를 생성해드릴까요?

Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (3)

38-39: 불필요한 nil 초기화 제거

Optional 변수는 기본적으로 nil로 초기화되므로 명시적인 = nil 초기화가 불필요합니다.

-    private var latitude: Double? = nil
-    private var longitude: Double? = nil
+    private var latitude: Double?
+    private var longitude: Double?

84-97: 파라미터 타입 일관성 개선

Input.selectPhoto는 non-optional Data를 받지만, selectPhoto 메서드는 Data?를 파라미터로 받고 있습니다. Input에서 이미 non-optional로 전달되므로 메서드 시그니처도 Data로 통일하는 것이 더 명확합니다.

-    private func selectPhoto(photoData: Data?) {
-        guard let photoData else { return }
+    private func selectPhoto(photoData: Data) {
         var currentSelectedPhoto = selectedPhotoSubject.value
         
         guard currentSelectedPhoto.count < 3 else {

80-82: 위치 설정 기능 구현 필요

현재 위치 설정 로직이 TODO로 남아있습니다. PR 설명에서 언급하신 것처럼, 앨범 사진의 경우 다른 위치에서 촬영되었을 수 있으므로 수동 위치 설정 UI가 필요합니다.

위치 설정 기능 구현을 위한 이슈를 생성해드릴까요?

Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (1)

23-28: 선택 상태 관리 로직 명확성 개선 고려

didSet의 early return 조건(!markIsSelected && selectedItem == nil)이 다소 복잡합니다. 이 로직은 markIsSelectedfalse일 때 선택이 해제되는 시점에만 delegate 호출을 막기 위한 것으로 보이지만, 조건의 의도가 명확하게 드러나지 않습니다. 주석 추가를 고려해보세요.

     private var selectedItem: T? {
         didSet {
+            // markIsSelected가 false인 경우, 선택 해제 시 delegate 알림 방지
             if !markIsSelected && selectedItem == nil { return }
             delegate?.selectableItemTableView(self, didSelectItem: selectedItem)
         }
     }
Projects/Presentation/Sources/Report/Model/ReportType+.swift (1)

10-36: 다국어 지원을 위한 지역화 고려

description 프로퍼티에 한국어 문자열이 하드코딩되어 있습니다. SelectPhotoType과 마찬가지로 향후 다국어 지원을 위해 Localizable.strings를 활용한 지역화를 고려해보세요.

 var description: String {
     switch self {
     case .lamp:
-        return "가로등"
+        return NSLocalizedString("report_type_lamp", comment: "")
     case .road:
-        return "도로"
+        return NSLocalizedString("report_type_road", comment: "")
     case .etc:
-        return "기타"
+        return NSLocalizedString("report_type_etc", comment: "")
     }
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5c3593 and a0caab1.

⛔ Files ignored due to path filters (24)
  • Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@1x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill@3x.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • Projects/Domain/Sources/Entity/Enum/ReportType.swift (1 hunks)
  • Projects/Domain/Sources/Entity/RoutineCompletionEntity.swift (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/Component/RequiredTitleLabel.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (2 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1 hunks)
  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/RoutineDeleteAlertView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • Projects/Presentation/Sources/Report/Model/PhotoItem.swift (1 hunks)
  • Projects/Presentation/Sources/Report/Model/ReportType+.swift (1 hunks)
  • Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/LocationButton.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportTextView.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1 hunks)
  • SupportingFiles/Info.plist (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (2)
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
  • selectCategory (82-90)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
  • action (84-117)
Projects/Presentation/Sources/Report/View/LocationButton.swift (2)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1)
  • configureAttribute (31-35)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)
  • configureAttribute (73-114)
Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (1)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)
  • reportPhotoCollectionViewCellWillDeleteCell (468-470)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • register (14-16)
Projects/Presentation/Sources/Report/View/ReportTextView.swift (2)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (3)
  • reportTextViewDidChanged (452-460)
  • reportTextViewDidTapped (462-464)
  • configureAttribute (73-114)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
  • chevronIcon (48-50)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (2)
Projects/Presentation/Sources/Report/View/LocationButton.swift (2)
  • configureAttribute (28-31)
  • configureLayout (33-40)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (2)
  • configureAttribute (73-114)
  • configureLayout (116-309)
Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (1)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)
  • selectableItemTableView (478-500)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (7)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (2)
  • configureCustomNavigationBar (19-29)
  • presentCustomBottomSheet (98-101)
Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (2)
  • configureAttribute (49-53)
  • configureLayout (55-61)
Projects/Presentation/Sources/Report/View/LocationButton.swift (2)
  • configureAttribute (28-31)
  • configureLayout (33-40)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (3)
  • configureAttribute (31-35)
  • configureLayout (37-51)
  • configure (53-73)
Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (3)
  • configureAttribute (44-61)
  • configureLayout (63-83)
  • configure (85-88)
Projects/Presentation/Sources/Report/View/ReportTextView.swift (3)
  • configureAttribute (49-88)
  • configureLayout (90-142)
  • configure (144-147)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (4)
  • action (51-66)
  • configureLocation (80-82)
  • removePhoto (99-102)
  • selectPhoto (84-97)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift

[Warning] 38-38: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 39-39: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift

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

(unused_closure_parameter)

⏰ 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 (19)
Projects/Presentation/Sources/Home/View/Component/RoutineDeleteAlertView.swift (1)

11-14: PR 목표와 파일 내용의 불일치를 확인해 주세요.

PR 설명에는 "제보하기 화면 구현"이 명시되어 있지만, 이 파일은 루틴 삭제 알림 뷰를 구현하고 있습니다. 이 컴포넌트가 의도적으로 함께 추가된 것인지 확인이 필요합니다.

Projects/Presentation/Resources/Images.xcassets/Report/Contents.json (1)

1-6: LGTM!

Report 애셋 카탈로그를 위한 표준 메타데이터 초기화 파일입니다.

Projects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/Contents.json (1)

1-23: LGTM!

round_delete_icon에 대한 표준 애셋 카탈로그 매니페스트입니다. 세 가지 스케일 변형(1x, 2x, 3x)이 올바르게 정의되었습니다.

Projects/Presentation/Sources/Report/View/LocationButton.swift (1)

11-41: LGTM!

위치 버튼 구현이 명확하고 간결합니다. 레이아웃과 속성 설정이 적절합니다.

SupportingFiles/Info.plist (1)

5-8: LGTM!

카메라 및 사진 라이브러리 접근을 위한 필수 권한 설명이 올바르게 추가되었습니다. 한국어 안내 문구가 명확하고 사용자 친화적입니다.

Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/Contents.json (1)

1-23: LGTM!

camera_icon에 대한 애셋 카탈로그 매니페스트가 올바르게 구성되었습니다.

Projects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/Contents.json (1)

1-23: LGTM!

location_icon에 대한 애셋 카탈로그 매니페스트가 올바르게 구성되었습니다.

Projects/Domain/Sources/Entity/Enum/ReportType.swift (1)

8-12: LGTM!

제보 타입을 나타내는 enum이 간결하고 명확하게 정의되어 있습니다. CaseIterable 프로토콜 준수로 모든 케이스를 순회할 수 있어 UI 컴포넌트와의 통합이 용이합니다.

Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)

38-38: 불변성 강화로 안전성 개선

outputlet으로 변경하여 초기화 후 재할당을 방지함으로써 ViewModel의 안전성이 향상되었습니다.

Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (1)

8-11: LGTM!

사진 선택 방식(카메라/라이브러리)을 나타내는 enum이 명확하게 정의되어 있습니다.

Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1)

13-29: LGTM!

Input/Output 패턴이 명확하게 정의되어 있으며, Combine 기반의 반응형 아키텍처가 잘 구현되어 있습니다.

Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (2)

22-22: LGTM!

markIsSelected 플래그 추가로 선택 표시 동작을 제어할 수 있게 되었습니다. 기본값이 true로 설정되어 기존 코드와의 호환성이 유지됩니다.

Also applies to: 32-36


92-104: 선택 처리 흐름 확인

markIsSelectedfalse일 때의 선택 처리 흐름이 올바르게 구현되어 있습니다:

  1. 사용자 선택 시 delegate에 알림
  2. 이후 selectedItemnil로 리셋하여 UI에 선택 표시 안 함
  3. reload 시 선택 표시가 나타나지 않음
Projects/Domain/Sources/Entity/RoutineCompletionEntity.swift (1)

8-28: LGTM!

루틴 완료 정보를 담는 entity가 명확하게 정의되어 있습니다. public 이니셜라이저로 다른 모듈에서의 사용이 용이합니다.

Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)

75-79: 새로운 아이콘 리소스 추가 확인

Report 기능에 필요한 3개의 아이콘이 기존 패턴과 일관되게 추가되었습니다.

Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (1)

15-89: LGTM!

사진 셀 구현이 잘 되어 있습니다. Delegate 패턴, 메모리 관리, prepareForReuse 처리 모두 적절합니다.

Projects/Presentation/Sources/Common/Component/RequiredTitleLabel.swift (1)

11-61: LGTM!

필수 입력 필드 타이틀 컴포넌트가 간결하고 명확하게 구현되었습니다.

Projects/Presentation/Sources/Report/View/ReportTextView.swift (1)

16-167: LGTM!

타입에 따른 텍스트 뷰 동작 구분, placeholder 처리, delegate 패턴이 모두 적절하게 구현되었습니다.

Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)

508-517: 이미지 압축 처리 확인

JPEG 압축률 0.5를 사용하여 메모리 사용량을 줄이는 것은 좋은 선택입니다. PR 목표에 언급된 메모리 중복 저장 문제와 관련하여, 현재 구현에서 이미지 Data가 여러 곳에 복사되는 것이 우려된다면, 향후 임시 파일 저장 방식으로 전환하는 것을 고려해볼 수 있습니다.

현재 메모리 사용 패턴이 실제로 문제가 되는지 Instruments를 사용하여 메모리 프로파일링을 진행해보시기 바랍니다. 특히 고해상도 이미지를 3장 선택했을 때의 메모리 사용량을 확인하세요.

Comment on lines +124 to +126
DIContainer.shared.register(type: ReportViewModel.self) { container in
return ReportViewModel()
}

@coderabbitai coderabbitai Bot Nov 9, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용하지 않는 클로저 파라미터를 언더스코어로 변경하세요.

ReportViewModel이 의존성을 필요로 하지 않아 container 파라미터가 사용되지 않습니다. SwiftLint 경고를 해결하기 위해 파라미터명을 _로 변경해주세요.

다음 diff를 적용하세요:

-        DIContainer.shared.register(type: ReportViewModel.self) { container in
+        DIContainer.shared.register(type: ReportViewModel.self) { _ in
             return ReportViewModel()
         }
📝 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
DIContainer.shared.register(type: ReportViewModel.self) { container in
return ReportViewModel()
}
DIContainer.shared.register(type: ReportViewModel.self) { _ in
return ReportViewModel()
}
🧰 Tools
🪛 SwiftLint (0.57.0)

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

(unused_closure_parameter)

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
around lines 124 to 126, the closure passed to DIContainer.shared.register
declares an unused parameter named `container`; rename this parameter to `_` so
the closure signature becomes `{ _ in ... }` (or simply `{ _ in return
ReportViewModel() }`) to silence the SwiftLint unused-parameter warning while
keeping behavior unchanged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

먼가 추후에 서버 붙이면서 container 쓸 것 같어 ~ 돈와리 !!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 알겠습니다! 향후 서버 연동 시 의존성을 추가할 계획이시라면 미리 container 파라미터를 유지하는 것도 좋은 선택입니다. SwiftLint 경고는 남아있겠지만, 나중에 코드를 수정하는 수고를 덜 수 있겠네요. 👍


🧠 Learnings used
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 30
File: Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift:42-43
Timestamp: 2025-07-30T03:56:18.617Z
Learning: choijungp는 현재 테스트 단계에서 RefreshTokenPlugin의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.

Comment on lines +120 to +122
private func scrollCollectionViewToEnd() {

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

사용되지 않는 메서드를 제거하세요.

이 메서드는 비어있고 호출되지 않는 데드 코드입니다. 코드베이스를 깔끔하게 유지하기 위해 제거해야 합니다.

다음 diff를 적용하여 제거하세요:

-    private func scrollCollectionViewToEnd() {
-
-    }
📝 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
private func scrollCollectionViewToEnd() {
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Home/View/Component/RoutineDeleteAlertView.swift
around lines 120 to 122, remove the unused empty method
scrollCollectionViewToEnd() (delete its entire declaration and surrounding blank
lines); ensure there are no remaining references to this method elsewhere
(search and remove calls if any) and run project build/tests after deletion to
confirm no compile errors.

Comment on lines +408 to +421
private func presentCamera() {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
let alertController = UIAlertController(title: "카메라 사용 불가", message: "이 기기에서 카메라를 사용할 수 없습니다.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "확인", style: .default))
present(alertController, animated: true)
return
}

let picker = UIImagePickerController()
picker.sourceType = .camera
picker.allowsEditing = false
picker.delegate = self
present(picker, animated: true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

카메라 권한 확인 누락

카메라를 표시하기 전에 카메라 권한 상태를 확인하지 않습니다. 권한이 거부된 상태에서 카메라를 열려고 하면 좋지 않은 사용자 경험을 제공합니다.

카메라를 표시하기 전에 AVCaptureDevice 권한을 확인하고, 필요시 권한 요청 또는 설정 안내를 추가하세요:

+import AVFoundation
+
 private func presentCamera() {
+    let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
+    
+    switch authStatus {
+    case .authorized:
+        showCameraPicker()
+    case .notDetermined:
+        AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
+            DispatchQueue.main.async {
+                if granted {
+                    self?.showCameraPicker()
+                } else {
+                    self?.showCameraPermissionDeniedAlert()
+                }
+            }
+        }
+    case .denied, .restricted:
+        showCameraPermissionDeniedAlert()
+    @unknown default:
+        break
+    }
+}
+
+private func showCameraPicker() {
     guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
         let alertController = UIAlertController(title: "카메라 사용 불가", message: "이 기기에서 카메라를 사용할 수 없습니다.", preferredStyle: .alert)
         alertController.addAction(UIAlertAction(title: "확인", style: .default))
         present(alertController, animated: true)
         return
     }
     
     let picker = UIImagePickerController()
     picker.sourceType = .camera
     picker.allowsEditing = false
     picker.delegate = self
     present(picker, animated: true)
 }
+
+private func showCameraPermissionDeniedAlert() {
+    let alertController = UIAlertController(
+        title: "카메라 권한 필요", 
+        message: "제보 사진을 촬영하려면 카메라 접근 권한이 필요합니다. 설정에서 권한을 허용해주세요.", 
+        preferredStyle: .alert
+    )
+    alertController.addAction(UIAlertAction(title: "설정으로 이동", style: .default) { _ in
+        if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
+            UIApplication.shared.open(settingsURL)
+        }
+    })
+    alertController.addAction(UIAlertAction(title: "취소", style: .cancel))
+    present(alertController, animated: true)
+}
📝 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
private func presentCamera() {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
let alertController = UIAlertController(title: "카메라 사용 불가", message: "이 기기에서 카메라를 사용할 수 없습니다.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "확인", style: .default))
present(alertController, animated: true)
return
}
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.allowsEditing = false
picker.delegate = self
present(picker, animated: true)
}
import AVFoundation
private func presentCamera() {
let authStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch authStatus {
case .authorized:
showCameraPicker()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
if granted {
self?.showCameraPicker()
} else {
self?.showCameraPermissionDeniedAlert()
}
}
}
case .denied, .restricted:
showCameraPermissionDeniedAlert()
@unknown default:
break
}
}
private func showCameraPicker() {
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
let alertController = UIAlertController(title: "카메라 사용 불가", message: "이 기기에서 카메라를 사용할 수 없습니다.", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "확인", style: .default))
present(alertController, animated: true)
return
}
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.allowsEditing = false
picker.delegate = self
present(picker, animated: true)
}
private func showCameraPermissionDeniedAlert() {
let alertController = UIAlertController(
title: "카메라 권한 필요",
message: "제보 사진을 촬영하려면 카메라 접근 권한이 필요합니다. 설정에서 권한을 허용해주세요.",
preferredStyle: .alert
)
alertController.addAction(UIAlertAction(title: "설정으로 이동", style: .default) { _ in
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
})
alertController.addAction(UIAlertAction(title: "취소", style: .cancel))
present(alertController, animated: true)
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Report/View/ReportViewController.swift around
lines 408 to 421, the presentCamera() method opens UIImagePickerController
without checking camera authorization; update it to first query
AVCaptureDevice.authorizationStatus(for: .video) and branch: if .authorized,
proceed to present the picker (and still guard
UIImagePickerController.isSourceTypeAvailable(.camera)); if .notDetermined, call
AVCaptureDevice.requestAccess(for: .video) and on granted present the picker on
the main thread; if .denied or .restricted, present an alert explaining the
permission is required with actions to cancel or open
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) so
the user can enable camera access in Settings; ensure all UI calls run on the
main thread.

Comment on lines +423 to +430
private func presentPhotoPicker() {
var config = PHPickerConfiguration(photoLibrary: .shared())
config.filter = .images
config.selectionLimit = 3
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

사진 선택 개수 제한과 실제 처리 로직 불일치

Line 426에서 selectionLimit = 3으로 최대 3장 선택을 허용하지만, PHPickerViewController delegate 구현부(lines 520-539)에서는 results.first만 처리하여 실제로는 1장만 추가됩니다. 사용자가 3장을 선택해도 1장만 등록되는 혼란스러운 UX가 발생합니다.

다음과 같이 수정하여 선택된 모든 사진을 처리하세요:

 extension ReportViewController: PHPickerViewControllerDelegate {
     func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
         picker.dismiss(animated: true, completion: nil)
-        guard
-            let itemProvider = results.first?.itemProvider,
-            itemProvider.canLoadObject(ofClass: UIImage.self)
-        else { return }
-
-        itemProvider.loadObject(ofClass: UIImage.self) { image, _ in
-            guard
-                let selectedImage = image as? UIImage,
-                let imageData = selectedImage.jpegData(compressionQuality: 0.5)
-            else { return }
-
-            DispatchQueue.main.async {
-                self.viewModel.action(input: .selectPhoto(photoData: imageData))
+        
+        for result in results {
+            let itemProvider = result.itemProvider
+            guard itemProvider.canLoadObject(ofClass: UIImage.self) else { continue }
+            
+            itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in
+                guard
+                    let selectedImage = image as? UIImage,
+                    let imageData = selectedImage.jpegData(compressionQuality: 0.5)
+                else { return }
+                
+                DispatchQueue.main.async {
+                    self?.viewModel.action(input: .selectPhoto(photoData: imageData))
+                }
             }
         }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Report/View/ReportViewController.swift around
lines 423-430 the picker is configured with selectionLimit = 3 but the delegate
only handles results.first (lines ~520-539), causing only one photo to be
processed; update the delegate to iterate over all PHPickerResult items instead
of using results.first, for each result use the NSItemProvider API
(loadObject(ofClass:UIImage.self) or loadDataRepresentation) to obtain the
image/data, convert to your attachment model and append each attachment to your
images array, ensure you synchronize async loads (DispatchGroup or serial async
handling) and update the UI on the main thread once each image/attachment is
added, and preserve the selectionLimit behaviour (max 3) by respecting
results.count and handling errors per-item without blocking processing of other
selected items.

@choijungp choijungp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

우선은 ~

  1. 이미지는 현재 방식도 충분히 노메러라고 생각이 들긴 하긴 합니다 .............. ㅠ ㅠ 조금 더 깊이 생각해볼게요
  2. SelectableItemTableView 확인 !!
    다만 코멘트에서도 남겼듯이 카메라 vs 앨범 선택을 SelectableItemTableView로 한다는 것이 확정되었는지 궁굼합니다 !!!
    글구 추가적으로 기획적인 부분에서 앨범이 추가되면서 위치 설정 UI 필요하다는거 완전 띵 ~...
    맞말인 것 같으요 !!! 짱 톡방에 함 물어볼게요 !!

최고 빠르다 .. 띵 감사함니다 !!
띵푸루부 !!! 👍🏻👍🏻👍🏻

Comment on lines +37 to +42
asteriskLabel.text = "*"
asteriskLabel.font = BitnagilFont.init(
style: .body1,
weight: .semiBold
).font
asteriskLabel.textColor = BitnagilColor.error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

요 부분 뭔가 .. 리브랜딩 전 디자인이라 바뀔 수도 있을 것 같아요 !!!
아마 .. asterisk_icon으로 ?? (제 추측입니당 !!)

그래도 RequiredTitleLabel 컴포넌트 빼서 하는거 넘 조은 것 같아요 !! 굿아 !!! 👍🏻

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

통일되면 ux면에서도 긍정적일 것 같습니다! 염두에 두고 있겠습니다~~!


private let itemTableView = UITableView()
private let items: [T]
private let markIsSelected: Bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

요 부분이 리뷰 노트에 언급해주신 카메라 vs 앨범 선택일 시에는 선택 결과를 보여주지 않기 위해 추가된 값이죵 ??
단순 궁금한게 있는데 카메라 vs 앨범 선택도 SelectableItemTable을 쓰기로 했나요 ??!!!

제가 금요일 회의를 참여하지 않아 정확한 상황을 파악하기 어려운 것 같아요 ㅠ.ㅠ
죄송함니다 ....... ..

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

그런 세세한 구현 내용은 다루지 않았지만,, 조이가 구현해두신 tableView + customBottomSheet를 사용하기 위해서는 SelectableItem을 채택한 프로토콜을 사용해야한다고 이해했습니다.!.!

사실 카메라 vs 앨범은 값을 선택해서 가지고 있는건 아니고, 일회성으로 선택하는 문제이긴 한데, 새로 tableView를 구현하기가 너무 귀찮았어요 ㅜㅜ

앞으로도 이런 화면이 추가적으로 나올 수도 있을 것 같은데, 새로 하나 구현해두는게 나을까요?

Comment on lines +124 to +126
DIContainer.shared.register(type: ReportViewModel.self) { container in
return ReportViewModel()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

먼가 추후에 서버 붙이면서 container 쓸 것 같어 ~ 돈와리 !!

}

func configure(imageCount: Int) {
let text = "\(imageCount)/3"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

개취이긴 한데 ~ 혹시 나아아중에 최대 이미지 개수가 수정될 가능성이 있을 수 있을지두 모루니까
3을 따로 상수로 빼는 것두 좋을 것 같아요 !!

굳이 해당 PR에서는 노메러라구 생각합니다 !! 그냥 개취 밝히기 ..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

아하 !! 그럼 추후에 이미지가 추가되면 ReportCameraButton의 configure 함수로 이미지 개수 업데이트 하는 거군용 !!
구웃 ~

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

상수로 빼는 것 아주 좋은 것 같습니다~! 매직넘버 지양 만세

Comment on lines +17 to +21
enum ReportTextViewType {
case combo
case editable
case nonEditable
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

헐 .. 제보하기에 필요한 텍스트를 ReportTextView로 분류한거 참 신기한 것 같어요 !!!

제보 카테고리, 위치 등록은 textView 처럼 동작하진 않지만 개발 편의성을 위해 ReportTextView로 통합한 것일까유 ???

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 맞습니다! 혹시 네이밍이 적절하지 않거나, 이렇게 합치는것 보다 나누는 것이 더 적절하다 생각하시면 편하게 말씀해주세요!

Comment on lines +88 to +91
guard currentSelectedPhoto.count < 3 else {
exceptionSubject.send("사진은 최대 3장까지 선택할 수 있습니다.")
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

요곳도 추후 3을 상수로 빼는 것두 조을 것 같다는 개인적 의견 ~..

Comment on lines +80 to +82
private func configureLocation() {
// 카카오 sdk로 현 위치 설정
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

요것두 단순 궁금한 것이 있는데요 ~..
디코 채팅방을 보니까 펨님이 "개발자들에게 gps 주소로 바꿔주는 거 티맵으로 하시길 하라. 통보하기" 라구 했던데 !!!

서버가 gps 주소 얻어서 클라한테 보내주눈 것인가요 ??

@taipaise taipaise Nov 9, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

  1. sdk를 어떤 걸 쓸지 확정은 안됐던것 같구, 카카오맵을 사용하는 방향이었던 것 같은데, 요건 한 번 더 여쭤보겠습니다.
  2. 세부 구현 내용은 역시 회의때 다루지는 않고, pm님이 보내주신 민관협력 지도 api 목록 을 확인해보면, 네이버, 구글, 카카오, 티맵이 있습니다. 각 링크를 누르면 해당 플랫폼에서 제공하는 지도sdk 레퍼런스로 넘어가게 되는데, 알아서 요거 보고 구현하면 될 것 같다~ 고 마무리되었습니다! 아마 내일부터 구현하게 되면, apple에서 제공하는 CoreLocation을 통해 위도와 경도 정보를 받고, 해당 정보를 카카오맵 SDK로 넘겨 현 위치의 주소를 받아 처리는 방식으로 구현할 것 같습니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (3)

408-421: 카메라 권한 확인이 누락되었습니다.

AVCaptureDevice 권한 상태를 확인하지 않고 카메라를 열면 권한이 거부된 상태에서 좋지 않은 사용자 경험을 제공합니다. 카메라 표시 전에 권한 확인 및 요청 로직을 추가하세요.


423-430: 사진 선택 개수 제한과 실제 처리 로직이 불일치합니다.

Line 426에서 selectionLimit = 3으로 설정했지만, PHPickerViewController delegate (lines 520-539)에서는 results.first만 처리하여 실제로는 1장만 추가됩니다.


432-435: 빈 조건문 블록을 제거하세요.

Lines 433-435의 if 블록이 비어있습니다. 불필요한 코드는 삭제하세요.

다음과 같이 수정하세요:

 private func dismissIfNeededThen(_ action: @escaping () -> Void) {
-    if let presentedViewController {
-        
-    }
-
     if Thread.isMainThread {
🧹 Nitpick comments (3)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (3)

38-38: 매직 넘버를 상수로 추출하는 것을 고려하세요.

maxPhotoCount = 3은 하드코딩되어 있습니다. 향후 변경 가능성을 고려하여 클래스 레벨의 상수나 설정 값으로 관리하는 것을 권장합니다.


39-40: 불필요한 nil 초기화를 제거하세요.

Optional 변수를 명시적으로 nil로 초기화하는 것은 불필요합니다. Swift는 자동으로 nil로 초기화합니다.

다음과 같이 수정할 수 있습니다:

-    private var latitude: Double? = nil
-    private var longitude: Double? = nil
+    private var latitude: Double?
+    private var longitude: Double?

81-83: 위치 설정 구현이 필요합니다.

카카오 SDK를 사용한 현재 위치 설정 로직이 아직 구현되지 않았습니다. PR 목표에 따르면 현재는 현재 위치만 지원하며, 수동 위치 선택 UI는 향후 개선 사항으로 계획되어 있습니다.

구현이 필요한 경우 도움을 드릴 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0caab1 and 4a8f484.

📒 Files selected for processing (3)
  • Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Presentation/Sources/Report/View/ReportCameraButton.swift
🧰 Additional context used
🧬 Code graph analysis (2)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (5)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (2)
  • configureCustomNavigationBar (19-29)
  • presentCustomBottomSheet (98-101)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (3)
  • configureAttribute (31-35)
  • configureLayout (37-51)
  • configure (53-73)
Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (3)
  • configureAttribute (44-61)
  • configureLayout (63-83)
  • configure (85-88)
Projects/Presentation/Sources/Report/View/ReportTextView.swift (3)
  • configureAttribute (49-88)
  • configureLayout (90-142)
  • configure (144-147)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (4)
  • action (52-67)
  • configureLocation (81-83)
  • removePhoto (100-103)
  • selectPhoto (85-98)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (2)
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
  • selectCategory (82-90)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
  • action (84-117)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift

[Warning] 39-39: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 40-40: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

⏰ 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 (13)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (8)

14-67: 클래스 선언과 속성 구성이 적절합니다.

뷰 컨트롤러의 구조가 명확하고 레이아웃 상수가 잘 정리되어 있습니다. Diffable data source를 사용한 컬렉션 뷰 구현도 현대적인 접근입니다.


73-114: 속성 구성이 잘 되어 있습니다.

UI 컴포넌트의 초기 설정과 액션 연결이 적절합니다.


116-309: 레이아웃 구성이 명확하고 적절합니다.

ScrollView와 SnapKit을 사용한 제약 조건 설정이 잘 되어 있습니다.


311-356: Combine 바인딩이 올바르게 구현되었습니다.

ViewModel의 output을 main thread에서 구독하고 UI를 업데이트하는 패턴이 적절합니다.


451-465: 텍스트 입력 델리게이트 구현이 적절합니다.

텍스트 변경 사항을 ViewModel에 전달하는 로직이 명확합니다.


467-471: 사진 삭제 델리게이트 구현이 적절합니다.

UUID를 사용한 사진 삭제 로직이 명확합니다.


477-501: 선택 항목 델리게이트 구현이 적절합니다.

타입별 분기 처리와 dismissIfNeededThen을 사용한 비동기 처리가 잘 구현되었습니다.


503-518: 카메라 이미지 피커 델리게이트 구현이 적절합니다.

JPEG 압축(0.5)을 적용하여 이미지 데이터를 처리하는 방식이 적절합니다.

Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (5)

12-30: Input/Output 패턴이 적절하게 구현되었습니다.

명확한 인터페이스 정의와 Combine publisher 사용이 적절합니다.


52-67: 액션 디스패처가 명확하게 구현되었습니다.

Input을 private 메서드로 라우팅하는 패턴이 깔끔합니다.


69-79: 상태 업데이트 메서드가 적절합니다.

카테고리, 제목, 내용을 subject로 전달하는 간단한 구현이 명확합니다.


85-98: 사진 선택 로직이 올바르게 구현되었습니다.

최대 개수 체크, PhotoItem 생성, 예외 메시지 전송이 적절하게 처리되어 있습니다.


100-103: 사진 제거 로직이 간결하고 명확합니다.

UUID 필터링을 통한 제거 로직이 적절합니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1)

34-34: 매직 넘버 3을 상수로 추출 권장

최대 사진 개수가 여러 곳에서 사용되므로 공통 상수로 관리하는 것이 좋습니다. ReportViewModel의 maxPhotoCount와 동일한 값을 참조하도록 개선할 수 있습니다.

Projects/Presentation/Sources/Report/View/ReportViewController.swift (2)

410-423: 카메라 권한 확인 누락

카메라를 표시하기 전에 AVCaptureDevice 권한 상태를 확인해야 합니다. 권한이 거부된 상태에서는 사용자에게 적절한 안내를 제공해야 합니다.

카메라 표시 전에 AVCaptureDevice.authorizationStatus(for: .video)를 확인하고, .notDetermined인 경우 권한 요청, .denied.restricted인 경우 설정 안내 알림을 표시하세요.


522-540: 사진 선택 개수 제한과 실제 처리 로직 불일치

Line 428에서 selectionLimit = 3으로 최대 3장 선택을 허용하지만, delegate 구현에서는 results.first만 처리하여 실제로는 1장만 추가됩니다.

선택된 모든 사진을 처리하도록 results를 순회하며 각 결과에 대해 loadObject를 호출하고 ViewModel에 전달하세요.

🧹 Nitpick comments (2)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1)

40-41: 불필요한 nil 초기화 제거 필요

옵셔널 변수는 자동으로 nil로 초기화되므로 명시적인 = nil 할당은 불필요합니다.

다음과 같이 수정하세요:

-    private var latitude: Double? = nil
-    private var longitude: Double? = nil
+    private var latitude: Double?
+    private var longitude: Double?
Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)

435-437: 불필요한 빈 코드 블록 제거

presentedViewController를 확인하는 빈 if 블록은 아무 동작도 하지 않습니다. 디버깅 중 남겨진 코드로 보입니다.

다음과 같이 제거하세요:

 private func dismissIfNeededThen(_ action: @escaping () -> Void) {
-    if let presentedViewController {
-        
-    }
-
     if Thread.isMainThread {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a8f484 and 6f3d5c7.

📒 Files selected for processing (3)
  • Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (2)
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
  • selectCategory (82-90)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
  • action (84-117)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (5)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (2)
  • configureCustomNavigationBar (19-29)
  • presentCustomBottomSheet (98-101)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (2)
  • configureAttribute (31-35)
  • configure (53-73)
Projects/Presentation/Sources/Report/View/ReportPhotoCollectionViewCell.swift (2)
  • configureAttribute (44-61)
  • configure (85-88)
Projects/Presentation/Sources/Report/View/ReportTextView.swift (2)
  • configureAttribute (49-88)
  • configure (144-147)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (4)
  • action (54-69)
  • configureLocation (83-85)
  • removePhoto (102-105)
  • selectPhoto (87-100)
Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (2)
Projects/Presentation/Sources/Report/View/ReportViewController.swift (2)
  • configureAttribute (73-114)
  • configureLayout (116-309)
Projects/Presentation/Sources/Report/View/LocationButton.swift (2)
  • configureAttribute (28-31)
  • configureLayout (33-40)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift

[Warning] 40-40: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)


[Warning] 41-41: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

⏰ 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)
Projects/Presentation/Sources/Report/ViewModel/ReportViewModel.swift (1)

54-105: 구현이 올바릅니다!

Input 처리 로직과 각 private 메서드들이 잘 구현되어 있습니다. 사진 선택 시 최대 개수 검증과 예외 처리가 적절하며, 삭제 로직도 정확합니다.

Projects/Presentation/Sources/Report/View/ReportViewController.swift (1)

311-358: View-ViewModel 바인딩이 잘 구현되었습니다!

Combine을 사용한 출력 바인딩이 체계적이며, 메인 스레드에서의 UI 업데이트 처리도 적절합니다. 각 publisher의 역할이 명확하게 구분되어 있습니다.

Projects/Presentation/Sources/Report/View/ReportCameraButton.swift (1)

53-73: AttributedString 구현이 정확합니다!

사진 개수 표시를 위한 속성 문자열 구성이 올바르게 구현되어 있으며, 첫 번째 문자의 색상을 다르게 처리하는 로직도 적절합니다.

@taipaise
taipaise merged commit f350a75 into develop Nov 9, 2025
2 checks passed
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.

2 participants