[Feat-T3-195] 제보하기 화면 구현 - #65
Conversation
Walkthrough사진 기반 신고 기능을 위해 도메인 엔티티(ReportType, RoutineCompletionEntity), 사진 모델(PhotoItem)과 뷰모델(ReportViewModel), 신고 화면(ReportViewController) 및 관련 UI 컴포넌트·아이콘 자산과 권한 설명이 추가되었습니다. DI에 ReportViewModel 등록이 포함됩니다. Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-optionalData를 받지만,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)이 다소 복잡합니다. 이 로직은markIsSelected가false일 때 선택이 해제되는 시점에만 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
⛔ Files ignored due to path filters (24)
Projects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/camera_icon.imageset/camera_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/location_icon.imageset/location_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Report/round_delete_icon.imageset/round_delete_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/navigation=home, status=fill@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/navigation=_mypage, status=fill@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/navigation=recommend, status=fill@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_empty_icon.imageset/navigation=report, status=empty@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/TabBarIcon/report_fill_icon.imageset/navigation=report, status=fill@3x.pngis 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: 불변성 강화로 안전성 개선
output을let으로 변경하여 초기화 후 재할당을 방지함으로써 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: 선택 처리 흐름 확인
markIsSelected가false일 때의 선택 처리 흐름이 올바르게 구현되어 있습니다:
- 사용자 선택 시 delegate에 알림
- 이후
selectedItem을nil로 리셋하여 UI에 선택 표시 안 함- 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장 선택했을 때의 메모리 사용량을 확인하세요.
| DIContainer.shared.register(type: ReportViewModel.self) { container in | ||
| return ReportViewModel() | ||
| } |
There was a problem hiding this comment.
사용하지 않는 클로저 파라미터를 언더스코어로 변경하세요.
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.
| 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.
There was a problem hiding this comment.
먼가 추후에 서버 붙이면서 container 쓸 것 같어 ~ 돈와리 !!
There was a problem hiding this comment.
@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의 토큰 갱신 로직 디버깅을 위해 액세스 토큰과 리프레시 토큰의 실제 값을 로그에 기록하는 것을 선호함.
| private func scrollCollectionViewToEnd() { | ||
|
|
||
| } |
There was a problem hiding this comment.
사용되지 않는 메서드를 제거하세요.
이 메서드는 비어있고 호출되지 않는 데드 코드입니다. 코드베이스를 깔끔하게 유지하기 위해 제거해야 합니다.
다음 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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
카메라 권한 확인 누락
카메라를 표시하기 전에 카메라 권한 상태를 확인하지 않습니다. 권한이 거부된 상태에서 카메라를 열려고 하면 좋지 않은 사용자 경험을 제공합니다.
카메라를 표시하기 전에 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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
사진 선택 개수 제한과 실제 처리 로직 불일치
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
left a comment
There was a problem hiding this comment.
우선은 ~
- 이미지는 현재 방식도 충분히 노메러라고 생각이 들긴 하긴 합니다 .............. ㅠ ㅠ 조금 더 깊이 생각해볼게요
- SelectableItemTableView 확인 !!
다만 코멘트에서도 남겼듯이 카메라 vs 앨범 선택을 SelectableItemTableView로 한다는 것이 확정되었는지 궁굼합니다 !!!
글구 추가적으로 기획적인 부분에서 앨범이 추가되면서 위치 설정 UI 필요하다는거 완전 띵 ~...
맞말인 것 같으요 !!! 짱 톡방에 함 물어볼게요 !!
최고 빠르다 .. 띵 감사함니다 !!
띵푸루부 !!! 👍🏻👍🏻👍🏻
| asteriskLabel.text = "*" | ||
| asteriskLabel.font = BitnagilFont.init( | ||
| style: .body1, | ||
| weight: .semiBold | ||
| ).font | ||
| asteriskLabel.textColor = BitnagilColor.error |
There was a problem hiding this comment.
요 부분 뭔가 .. 리브랜딩 전 디자인이라 바뀔 수도 있을 것 같아요 !!!
아마 .. asterisk_icon으로 ?? (제 추측입니당 !!)
그래도 RequiredTitleLabel 컴포넌트 빼서 하는거 넘 조은 것 같아요 !! 굿아 !!! 👍🏻
There was a problem hiding this comment.
통일되면 ux면에서도 긍정적일 것 같습니다! 염두에 두고 있겠습니다~~!
|
|
||
| private let itemTableView = UITableView() | ||
| private let items: [T] | ||
| private let markIsSelected: Bool |
There was a problem hiding this comment.
요 부분이 리뷰 노트에 언급해주신 카메라 vs 앨범 선택일 시에는 선택 결과를 보여주지 않기 위해 추가된 값이죵 ??
단순 궁금한게 있는데 카메라 vs 앨범 선택도 SelectableItemTable을 쓰기로 했나요 ??!!!
제가 금요일 회의를 참여하지 않아 정확한 상황을 파악하기 어려운 것 같아요 ㅠ.ㅠ
죄송함니다 ....... ..
There was a problem hiding this comment.
그런 세세한 구현 내용은 다루지 않았지만,, 조이가 구현해두신 tableView + customBottomSheet를 사용하기 위해서는 SelectableItem을 채택한 프로토콜을 사용해야한다고 이해했습니다.!.!
사실 카메라 vs 앨범은 값을 선택해서 가지고 있는건 아니고, 일회성으로 선택하는 문제이긴 한데, 새로 tableView를 구현하기가 너무 귀찮았어요 ㅜㅜ
앞으로도 이런 화면이 추가적으로 나올 수도 있을 것 같은데, 새로 하나 구현해두는게 나을까요?
| DIContainer.shared.register(type: ReportViewModel.self) { container in | ||
| return ReportViewModel() | ||
| } |
There was a problem hiding this comment.
먼가 추후에 서버 붙이면서 container 쓸 것 같어 ~ 돈와리 !!
| } | ||
|
|
||
| func configure(imageCount: Int) { | ||
| let text = "\(imageCount)/3" |
There was a problem hiding this comment.
개취이긴 한데 ~ 혹시 나아아중에 최대 이미지 개수가 수정될 가능성이 있을 수 있을지두 모루니까
3을 따로 상수로 빼는 것두 좋을 것 같아요 !!
굳이 해당 PR에서는 노메러라구 생각합니다 !! 그냥 개취 밝히기 ..
There was a problem hiding this comment.
아하 !! 그럼 추후에 이미지가 추가되면 ReportCameraButton의 configure 함수로 이미지 개수 업데이트 하는 거군용 !!
구웃 ~
There was a problem hiding this comment.
상수로 빼는 것 아주 좋은 것 같습니다~! 매직넘버 지양 만세
| enum ReportTextViewType { | ||
| case combo | ||
| case editable | ||
| case nonEditable | ||
| } |
There was a problem hiding this comment.
헐 .. 제보하기에 필요한 텍스트를 ReportTextView로 분류한거 참 신기한 것 같어요 !!!
제보 카테고리, 위치 등록은 textView 처럼 동작하진 않지만 개발 편의성을 위해 ReportTextView로 통합한 것일까유 ???
There was a problem hiding this comment.
넵 맞습니다! 혹시 네이밍이 적절하지 않거나, 이렇게 합치는것 보다 나누는 것이 더 적절하다 생각하시면 편하게 말씀해주세요!
| guard currentSelectedPhoto.count < 3 else { | ||
| exceptionSubject.send("사진은 최대 3장까지 선택할 수 있습니다.") | ||
| return | ||
| } |
There was a problem hiding this comment.
요곳도 추후 3을 상수로 빼는 것두 조을 것 같다는 개인적 의견 ~..
| private func configureLocation() { | ||
| // 카카오 sdk로 현 위치 설정 | ||
| } |
There was a problem hiding this comment.
요것두 단순 궁금한 것이 있는데요 ~..
디코 채팅방을 보니까 펨님이 "개발자들에게 gps 주소로 바꿔주는 거 티맵으로 하시길 하라. 통보하기" 라구 했던데 !!!
서버가 gps 주소 얻어서 클라한테 보내주눈 것인가요 ??
There was a problem hiding this comment.
- sdk를 어떤 걸 쓸지 확정은 안됐던것 같구, 카카오맵을 사용하는 방향이었던 것 같은데, 요건 한 번 더 여쭤보겠습니다.
- 세부 구현 내용은 역시 회의때 다루지는 않고, pm님이 보내주신 민관협력 지도 api 목록 을 확인해보면, 네이버, 구글, 카카오, 티맵이 있습니다. 각 링크를 누르면 해당 플랫폼에서 제공하는 지도sdk 레퍼런스로 넘어가게 되는데, 알아서 요거 보고 구현하면 될 것 같다~ 고 마무리되었습니다! 아마 내일부터 구현하게 되면, apple에서 제공하는
CoreLocation을 통해 위도와 경도 정보를 받고, 해당 정보를카카오맵 SDK로 넘겨 현 위치의 주소를 받아 처리는 방식으로 구현할 것 같습니다!
There was a problem hiding this comment.
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
📒 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 필터링을 통한 제거 로직이 적절합니다.
There was a problem hiding this comment.
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
📒 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 구현이 정확합니다!사진 개수 표시를 위한 속성 문자열 구성이 올바르게 구현되어 있으며, 첫 번째 문자의 색상을 다르게 처리하는 로직도 적절합니다.
🌁 Background
📱 Screenshot
👩💻 Contents
📝 Review Note
개발적인 부분
작년에도 함께 고민하던 부분이었던 것 같은데요, 요 이미지를 어떻게 처리해야할 지 굉장히 고민입니다. 일단은 메인메모리에서 이미지 데이터를 바이너리 형식으로 들고 있기는 한데요, 개선이 필요해 보입니다.
현재는 아래와 같은 과정을 통해 이미지를 처리합니다.
viewModel에 이미지의 바이너리 데이터를 전달합니다.ViewModel은 바이너리 데이터를CurrentValueSubject에 저장하고, 이 정보를 publish합니다.ViewController는 다시 이 정보를 받아서datasource의snapshot을apply합니다.막연하게 지금
ViewModel에서Subject에서 한 번,ViewController에서DataSource에서 한 번, 마지막으로 사진을 표시하는Cell에서UIImage형태로 한 번, 같은 이미지의 바이너리 데이터를 총 세 번이나 메인메모리에서 들고 있지 않을까 했습니다.지피티에 따르면,
Data는 Copy-on-Write라서 수정하지 않는 한 바이트 버퍼는 공유되기 때문에, 별도로 메모리를 몇 배로 먹지 않는다고 하긴하는데,, 이미지 처리 방법에 대해 좀 더 공부할 필요가 있어보입니다.고려했던 방법으로는 일단 이미지를 파일시스템에 저장해두고 (앱 샌드박스의 temp 공간)이미지를 표시하거나 전송할 때 파일 입출력을 통해 처리해볼까? 했는데 바람직한 방법이라는 확신이 들지 않습니다.
기획 적인 부분
개선 하면 좋을 점 (데스노트 추가하겠습니다!)
Summary by CodeRabbit
새로운 기능
개선 사항