Skip to content

Conversation

@taipaise
Copy link
Collaborator

@taipaise taipaise commented Nov 22, 2025

🌁 Background

  • 바로 직전 PR에 이어지는 PR입니다!!

👩‍💻 Contents

  • 제보상세 ViewModel을 재활용하여 제보 완료 화면에서 제보 내용을 불러오도록 함
  • 번거로우시겠지만, [Feat-T3-204] 제보하기 로직 구현, 제보 히스토리 로직 구현 #73제보하기 완료 이후 플로우 (논의 필요) 부분을 먼저 검토해주시면 감사하겠습니다.
  • 만약 해당 방향대로 진행한다면, 앞 pr 반영 이후 해당 pr 반영하도록 하겠습니다.

Summary by CodeRabbit

릴리스 노트

  • 개선사항

    • 신고 등록 화면에서 입력 유효성 검사와 등록 버튼 활성화가 실시간으로 동작합니다.
    • 신고 완료 후 로딩 화면이 등록 결과를 받아 이동하거나 뒤로 돌아가는 흐름을 지원합니다.
    • 신고 상세 화면이 뷰모델 출력에 따라 제목·카테고리·위치·설명·사진을 동적으로 표시합니다.
    • 사진 업로드 과정이 추가되어 제출 시 사진이 전송됩니다.
  • 신규

    • 카메라·사진 아이콘 자산 및 선택 항목 아이콘 표시가 추가되었습니다.
    • 신고 작성 시 글자 수 표시(최대 150자)가 표시됩니다.
  • 리팩토링

    • 상세·등록 관련 화면 흐름과 데이터 바인딩을 반응형으로 정리했습니다.

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

@taipaise taipaise requested a review from choijungp November 22, 2025 11:29
@taipaise taipaise self-assigned this Nov 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

Walkthrough

파일 업로드(사전 서명 URL) 기능과 리포트 생성/조회 흐름이 도입되었고, 도메인·레포지토리·엔드포인트·ViewModel이 확장·변경되었습니다. UI 컴포넌트·이미지 자산·DI 등록 및 네트워크 바디 타입(rawData/json) 처리가 추가·조정되어 전체 리포트 등록·업로드·조회 경로가 재구성되었습니다.

Changes

Cohort / File(s) 변경 요약
리포트 화면·등록/완료/로딩 변경
Projects/Presentation/Sources/Report/View/...
등록 흐름 추가: ReportRegistrationViewController.swift(delegate, UI/검증/퍼블리셔), ReportLoadingViewController.swift(delegate 처리), ReportCompleteViewController.swift(BaseViewController, Combine 바인딩, photo UIImageView 반환 변경), ReportHistoryViewController.swift(그룹 키/애니메이션 변경), ReportHistoryTableViewCell.swift(selectionStyle 변경)
Report ViewModel 변경들
Projects/Presentation/Sources/Report/ViewModel/...
ReportRegistrationViewModel(isReportValid, reportRegistrationCompletePublisher, register/검증 흐름), ReportDetailViewModel(date 포맷·thumbnail binding), ReportHistoryViewModel(selectedProgress 상태·progress 재계산)
파일 업로드 관련 엔드포인트·DTO·리포지토리
Projects/DataSource/Sources/Endpoint/..., Projects/DataSource/Sources/DTO/..., Projects/DataSource/Sources/Repository/FileRepository.swift
새 엔드포인트: FilePresignedEndpoint.swift(presigned-urls POST, rawData bodyType), S3UploadEndpoint.swift(PUT rawData upload). DTO 추가: FilePresignedConditionDTO.swift, FilePresignedDTO.swift. 리포지토리: FileRepository 구현(사전서명 URL 조회·업로드)
리포트 엔드포인트·리포지토리·UseCase 변경
Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift, Projects/DataSource/Sources/Repository/ReportRepository.swift, Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift
ReportEndpointregister(report: ReportDTO) 추가(POST); ReportRepository API 변경(파라미터 기반 report(...)-> Int? 반환, throws); ReportUseCase에 FileRepository 주입 및 사진 업로드 흐름 추가, report(...) 시 presigned URL 조회·업로드·report 호출
도메인 모델·프로토콜 변경
Projects/Domain/Sources/...
ReportEntity(id 옵셔널, thumbnailURL 추가, photoURLs로 명칭 변경), ReportType rawValue 및 CustomStringConvertible, ReportRepositoryProtocolReportUseCaseProtocol 시그니처 변경/추가, FileRepositoryProtocol 추가
네트워크·Endpoint 처리 변경
Projects/DataSource/Sources/NetworkService/...
Endpoint 프로토콜에 bodyType/bodyData 추가(기본 .json/nil). Endpoint+.swift에서 bodyType에 따라 json 파라미터 빌드 또는 rawData를 httpBody에 직접 설정. NetworkService에서 EmptyResponseDTO 특수 처리 추가.
DTO·매핑 변경
Projects/DataSource/Sources/DTO/...
ReportDTO가 Codable로 변경, reportStatus 옵셔널화, thumbnailURL 매핑 추가, toReportEntity 변환 로직 조정
DI(의존성 주입) 변경
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift, Projects/Domain/Sources/DomainDependencyAssembler.swift
FileRepositoryProtocol 등록 추가 및 ReportUseCase 생성 시 fileRepository 주입
UI 컴포넌트·프로토콜·아이콘·애셋
Projects/Presentation/Sources/Common/..., Projects/Presentation/Resources/Images.xcassets/Report/*
SelectableItem 프로토콜에 icon 추가(디폴트 nil), SelectableItemCell 레이아웃/아이콘 처리 업데이트, BitnagilIcon에 camera/photo 아이콘 추가, 이미지 자산(카메라·포토) 추가
공유 유틸·포맷 변경
Projects/Shared/Sources/Extension/Date+.swift
Date.DateType에 yearMonthDateWeekyearMonthDateWeek2 포맷 추가
기타 변경
여러 파일
ReportRegistrationViewController 관련 콘텐츠 카운트/placeholder/레이아웃 조정, ReportTextView chevron 크기/이미지 변경 등 UI 세부 조정

Sequence Diagram(s)

sequenceDiagram
    participant UI as ReportRegistrationVC
    participant VM as ReportRegistrationVM
    participant UC as ReportUseCase
    participant FR as FileRepository
    participant RR as ReportRepository
    participant NS as NetworkService

    UI->>VM: 사용자 입력/사진 선택 / register()
    VM->>UC: report(title, content, category, location, photos)
    UC->>FR: fetchPresignedURL(prefix, filenames)
    FR-->>UC: presignedDict (filename -> presignedURL)
    UC->>FR: uploadFile(url: presignedURL, data)
    FR->>NS: S3Endpoint.uploadImage (rawData PUT)
    NS-->>FR: EmptyResponseDTO
    UC->>RR: report(..., photoURLs) (ReportEndpoint.register POST)
    RR->>NS: performRequest (ReportEndpoint.register)
    NS-->>RR: BaseResponse / id
    RR-->>UC: id
    UC-->>VM: reportId
    VM-->>UI: reportRegistrationCompletePublisher (reportId)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • 주된 이유: 도메인 모델·프로토콜 API 변동(여러 퍼블릭 시그니처 변경), 리포지토리·UseCase 로직 추가(비동기 파일 업로드·에러 처리 경로), 네트워크 레이어(bodyType/rawData) 변경 등이 광범위하고 상호의존적임.
  • 주의할 파일/영역:
    • ReportUseCase.swift (사진 업로드 시 동기화·오류·파일명 생성 로직)
    • ReportRepository.swift / ReportRepositoryProtocol (throws/반환값 변경에 따른 호출부 영향)
    • NetworkServiceEndpoint+.swift (rawData 처리, EmptyResponseDTO 특수 처리)
    • ReportEntity / ReportDTO 매핑(옵셔널 id·thumbnailURL 일치 여부)
    • DI 어셈블러들 (FileRepository 주입 누락/순환 의존 여부)

🐰 새 길에 깡총깡총 달려가
서명 URL 구해 찍고 올려요
리포트는 작성되었네, 빛나네 ✨
토끼가 축하의 당근을 나눠요
변화를 맞이하니 마음이 폴짝!

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 제목은 "[Feat] 제보 완료화면 ViewModel 추가"로, 변경사항의 주요 내용인 ReportCompleteViewController에서 ViewModel을 활용하여 제보 완료 화면을 개선하는 부분을 명확히 요약하고 있습니다.
✨ 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-registration-completion

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (1)

261-270: 사진 썸네일 표현 품질을 위해 contentMode 설정을 고려해 보세요.

현재 UIImageView에 배경색·cornerRadius만 지정되어 있어 실제 이미지를 로드하면 비율이 깨질 수 있습니다. 썸네일 용도라면 scaleAspectFill + clipsToBounds(true) 조합이 일반적입니다 (masksToBounds = true는 이미 설정되어 있으니 contentMode만 추가해도 효과가 있을 것 같습니다).

private func makePhotoView() -> UIImageView {
    let photoView = UIImageView()
    photoView.backgroundColor = BitnagilColor.gray30
    photoView.layer.masksToBounds = true
    photoView.layer.cornerRadius = 6
+   photoView.contentMode = .scaleAspectFill
    photoView.snp.makeConstraints { make in
        make.size.equalTo(Layout.photoSize)
    }
    return photoView
}
📜 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 da2ae77 and abe4b88.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.
📚 Learning: 2025-11-19T13:49:08.326Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
🧬 Code graph analysis (1)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (2)
Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift (4)
  • viewDidLoad (71-74)
  • configureAttribute (76-95)
  • configureLayout (97-136)
  • bind (138-145)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
  • attributedString (50-67)
⏰ 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/View/ReportCompleteViewController.swift (3)

8-12: BaseViewController 제네릭 + DI 패턴 적용이 일관되고 적절합니다.

BaseViewController<ReportDetailViewModel> 상속과 init(viewModel:reportId:) DI 패턴, 그리고 cancellables 보관 방식이 ReportDetailViewController와 일관되어 구조적으로 잘 맞습니다. 스토리보드를 쓰지 않는 화면이므로 fatalError 기반 coder-init 처리도 지금 컨텍스트에서는 무리 없어 보입니다.

Also applies to: 69-80


89-119: 기존 UI 속성 설정을 유지하면서 BaseViewController 패턴에 맞게 잘 정리되어 있습니다.

configureAttribute()를 override로 전환하면서 배경색, 폰트, 색상, 스택뷰 속성 등이 이전 구현 그대로 유지되고 있어, 화면 전반의 스타일링이 BaseViewController 라이프사이클과 자연스럽게 녹아듭니다.


121-185: 레이아웃 구성 및 fomo/background 오버랩 구조가 의도와 잘 맞습니다.

  • scrollViewcontentViewcontentLayoutGuide/frameLayoutGuide로 잡은 구조가 스크롤·오토레이아웃 관점에서 정석적인 패턴입니다.
  • Line 168–171에서 backgroudView.topfomoImageView(top) 기준으로 두고 오프셋을 주는 방식은, 이전에 공유된 “fomoImageView와 backgroundView가 겹치는 연출” 의도와도 부합해 보입니다.

Based on learnings

Comment on lines 82 to 87
override func viewDidLoad() {
super.viewDidLoad()
configureAttribute()
configureLayout()
fetchReport()
viewModel.action(input: .fetchReportDetail(reportId: reportId))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

configureAttribute/configureLayout가 두 번 호출될 가능성이 있습니다.

BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하는 패턴이라면, Line 84–85에서 직접 다시 호출하면서 서브뷰 중복 추가·제약 중복 설정이 발생할 수 있습니다. 실제로 레이아웃 쪽은 두 번 도는 순간 Auto Layout 경고나 예기치 않은 뷰 계층 구조가 생길 수 있습니다.

이 컨트롤러도 ReportDetailViewController와 동일하게 super.viewDidLoad() 이후에는 액션만 호출하도록 정리하는 편이 안전해 보입니다.

 override func viewDidLoad() {
     super.viewDidLoad()
-    configureAttribute()
-    configureLayout()
     viewModel.action(input: .fetchReportDetail(reportId: reportId))
 }
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
around lines 82–87, remove the duplicate calls to configureAttribute() and
configureLayout() in viewDidLoad() because BaseViewController already invokes
those (and bind()), which can cause duplicate subview additions and constraint
warnings; keep super.viewDidLoad() and only call the report fetch action
(viewModel.action(input: .fetchReportDetail(reportId: reportId))), or
alternatively guard against double-initialization if BaseViewController behavior
changes.

@taipaise
Copy link
Collaborator Author

taipaise commented Nov 22, 2025

CI 과정 발생한 오류는, 앞 PR 머지 이후 rebase 하면서 같이 작업하는게 좋을 것 같습니다~

taipaise and others added 4 commits November 23, 2025 14:35
* feat: NetworkService 로직 수정

- request 생성로직 수정. body에 jsonData 뿐 아니라, rawdata도 들어갈 수 있도록 수정 (s3 업로드 위함)
- response body가 empty여도 error를 throw 하지 않도록 수정

* feat: 제보하기 로직 구현

* refactor: 제보하기 수정된 디자인 적용

* feat: 제보하기 필터링 로직 구현

* feat: 제보히스토리 -> 제보상세 플로우 미비사항 구현

- 진행상황 collectionView cell에 진행상황 별 갯수 표시
- ReportEntity id 값을 옵셔널로 변경
- ReportDetail 날짜 포멧 변경

* fix: 제보하기 로직 수정

* refactor: 코드래빗 리뷰 반영
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)

75-102: 셀 재사용 시 아이콘 상태 및 displayName 조건 때문에 아이콘이 잘못 보일 수 있습니다

현재 configureCell 구현에는 두 가지 잠재적인 문제가 있습니다.

  1. displayName == nil 인 항목은 아이콘 로직을 타지 않습니다

    • guard let displayName = item.displayName else { ... return } 이후에만
      if let icon = item.icon {
          iconImageView.image = icon
          iconImageView.isHidden = false
      }
      가 호출됩니다.
    • SelectPhotoTypedisplayName == nil, icon 은 non-nil 이므로, 아이콘을 구현했음에도 실제로는 한 번도 보이지 않게 됩니다.
  2. 셀 재사용 시 이전 아이콘이 남을 수 있습니다

    • 어떤 아이템에서 icon 이 설정되어 iconImageView.isHidden = false 가 된 뒤,
      다음에 icon == nil 인 아이템으로 재사용되면, else 쪽에서 isHidden/image 를 리셋하지 않아 이전 아이콘이 그대로 남을 수 있습니다.
    • displayName == nil 경로에서는 아예 아이콘에 손대지 않고 곧바로 return 하기 때문에, 여기서도 같은 문제가 발생할 수 있습니다.

두 문제를 함께 해결하려면, 아이콘 설정/리셋을 guard 앞에서 공통으로 처리하는 것이 좋습니다. 예시는 다음과 같습니다.

 func configureCell(item: SelectableItem, isSelected: Bool) {
     checkIcon.isHidden = !isSelected
 
-    guard let displayName = item.displayName else {
-        titleLabel.text = item.description
-        return
-    }
-
-    if let icon = item.icon {
-        iconImageView.image = icon
-        iconImageView.isHidden = false
-    }
+    // 아이콘은 displayName 여부와 상관없이 항상 갱신
+    let icon = item.icon
+    iconImageView.image = icon
+    iconImageView.isHidden = (icon == nil)
+
+    guard let displayName = item.displayName else {
+        titleLabel.attributedText = nil
+        titleLabel.text = item.description
+        return
+    }
 
     let attributedString = NSMutableAttributedString(string: item.description)
     attributedString.addAttribute(
         .font,
         value: BitnagilFont(style: .body1, weight: .regular).font,
         range: NSRange(location: 0, length: item.description.count))
@@
-    titleLabel.attributedText = attributedString
+    titleLabel.attributedText = attributedString
 }

이렇게 정리하면

  • displayName 이 없는 항목(예: SelectPhotoType)도 아이콘은 정상 노출되고,
  • 셀 재사용 시에도 항상 현재 item.icon 기준으로 이미지/hidden 상태가 초기화되어 잘못된 아이콘이 남지 않게 됩니다.
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)

116-125: 등록 버튼 탭 시 로딩 화면/델리게이트 흐름은 좋지만, 한 가지 컴파일 이슈와 중복 실행 가능성이 있습니다

  1. reportRegistrationCompletePublisher 바인딩에서 delegate 앞에 self.가 빠져 있어 컴파일이 안 될 것 같습니다.
        viewModel.output.reportRegistrationCompletePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] reportId in
                guard let self else { return }

-                delegate?.reportRegistrationViewController(self, completeRegistration: reportId)
+                self.delegate?.reportRegistrationViewController(self, completeRegistration: reportId)
            }
            .store(in: &cancellables)
  1. 현재 구현에서는 등록 버튼을 여러 번 빠르게 탭하면 ReportLoadingViewController()가 그만큼 push되고, register()도 중복 호출될 수 있습니다. 실제 네트워크/서버 상태에 따라 중복 제보가 생길 여지가 있어서,
    • 첫 탭 이후에는 버튼 상태를 .inProgress 혹은 .disabled로 바꾸고,
    • 등록 완료/실패 시 다시 활성화
      하는 식으로 in‑flight 상태를 ViewModel에서 관리해 주면 더 안전할 것 같습니다.

Also applies to: 407-425

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

83-88: configureAttribute/configureLayoutviewDidLoad에서 다시 호출하면 중복 초기화가 발생할 수 있습니다

BaseViewControllerviewDidLoad()에서 이미 configureAttribute(), configureLayout(), bind()를 호출하는 패턴이라면, 여기서 다시 직접 호출하면 서브뷰 추가·제약 설정이 두 번씩 일어나 Auto Layout 경고나 예기치 않은 UI 결과가 나올 수 있습니다 (이전에 같은 이슈가 한 번 지적된 적 있습니다).

이 컨트롤러도 ReportDetailViewController와 동일하게 super.viewDidLoad() 이후에는 액션만 호출하도록 정리하는 것이 안전해 보입니다.

    override func viewDidLoad() {
        super.viewDidLoad()
-        configureAttribute()
-        configureLayout()
        viewModel.action(input: .fetchReportDetail(reportId: reportId))
    }
🧹 Nitpick comments (17)
Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (1)

8-9: 카메라/앨범 타입별 아이콘 매핑 무난합니다

SelectableItem.icon 요구사항에 맞게 BitnagilIcon 을 사용해 명확히 매핑되어 있고, 옵셔널 처리도 자연스럽습니다.
템플릿 컬러로 틴팅해야 하는 디자인이라면, 에셋 쪽에서 withRenderingMode(.alwaysTemplate) 적용 여부만 한 번 확인해 두면 좋겠습니다.

Also applies to: 38-45

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

79-80: 리포트용 카메라/사진 아이콘 추가 이상 없습니다

새 에셋 이름(bitnagil_camera_icon, bitnagil_photo_icon)과 SelectPhotoType.icon 사용처가 일관되고, 다른 아이콘 정의 방식과도 형식이 동일해서 유지보수에 문제 없어 보입니다.
필요하다면 이 아이콘도 다른 곳처럼 템플릿 렌더링(withRenderingMode(.alwaysTemplate))을 사용할지 디자인 가이드에 맞춰 통일만 고민해 보시면 될 것 같습니다.

Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)

1-23: Presigned URL 반환 타입의 옵셔널 여부 한 번만 정리해 보면 좋겠습니다

fetchPresignedURL[String: String]? 를 반환하도록 설계되어 있는데, FileRepository 구현에서는 현재 항상 non-optional 딕셔너리를 리턴하고 있어 실제로 nil 이 나올 상황이 거의 없어 보입니다.

  • presigned URL 발급이 실패하면 throw 로 처리하고
  • 성공 시에는 항상 모든 파일 이름에 대한 URL 을 받는 구조라면

반환 타입을 [String: String] 로 단순화해도 좋을 것 같습니다. 반대로, 일부 케이스에서 응답 바디가 없거나 비어 있을 수 있다면, 그 시나리오를 주석에 조금 더 명시해 두면 호출 측에서 이해하기 쉬울 것 같습니다.

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

14-18: 스택뷰 기반 레이아웃 전환은 괜찮습니다 (axis/size 세부 설정만 참고)

stackView 로 아이콘 + 타이틀을 묶고, 고정 높이/마진을 주는 구조는 명확하고 유지보수도 좋아 보입니다.
소소하게 고려해 볼 만한 부분은 아래 정도입니다.

  • stackView.axis 는 기본값이 .horizontal 이라 현재도 동작에는 문제 없지만, 명시적으로 .horizontal 을 지정해 두면 의미가 더 분명해집니다.
  • iconImageView 는 크기를 20pt 정사각형으로 고정해 두셨는데, 아이콘 에셋 비율에 따라 간혹 찌그러질 수 있어 contentMode = .scaleAspectFit 정도를 설정해 두면 안전합니다.
  • stackViewwidth == 0 (priority .medium) 제약을 주신 부분은 레이아웃 경고 방지용 의도로 보이는데, 나중에 디버깅할 때 혼란을 줄 수 있으니, 실제로 필요한지 한 번 더 확인해 보시고 필요 없다면 제거하는 것도 옵션입니다.

전반적으로 기능적인 문제는 없어 보이고, 위 내용은 선택적인 정리 사항에 가깝습니다.

Also applies to: 20-22, 35-49, 51-66

Projects/DataSource/Sources/Repository/ReportRepository.swift (1)

14-38: 신규 report 구현은 일관적이지만, nil id 의미를 명확히 해두는 편이 좋습니다

ReportDTO 구성과 ReportEndpoint.register 호출 흐름은 ReportUseCase.report와 잘 맞습니다. 다만 networkService.request 결과가 nil인 경우를 그대로 nil id로 올려보내고 있어서, 상위 레이어에서 “요청 실패(에러)”와 “정상 응답이지만 id 없음”을 구분하기가 애매할 수 있습니다.

도메인에서 이 함수를 호출하는 쪽과 논의해,

  • “id가 반드시 내려와야 한다”는 계약이라면 nil 대신 명시적인 도메인 에러를 throw 하거나,
  • “id가 없을 수도 있다”는 스펙이라면 그 의미를 주석/문서에 남겨 두는 것
    을 고려해 보시면 좋겠습니다.
Projects/Domain/Sources/Entity/Enum/ReportType.swift (1)

8-18: rawValue가 이미 대문자라면 uppercased()는 불필요합니다

지금 케이스들의 rawValue"TRANSPORTATION", "LIGHTING" 등 이미 모두 대문자라서, description에서 한 번 더 uppercased()를 호출할 필요는 없습니다. 동작에는 문제가 없지만, 간결성을 위해 아래처럼 단순화할 수 있습니다.

-extension ReportType: CustomStringConvertible {
-    public var description: String {
-        return self.rawValue.uppercased()
-    }
-}
+extension ReportType: CustomStringConvertible {
+    public var description: String {
+        rawValue
+    }
+}

추후 소문자 rawValue를 도입해 API용 문자열을 따로 관리하고 싶다면, description 대신 apiValue 같은 별도 프로퍼티를 두는 것도 고려해 볼 수 있습니다.

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

83-95: 제보 완료 화면 네비게이션 플로우 및 TODO 정리 제안

reportId 유무에 따라 완료 화면으로 push / 현재 VC pop 처리하는 delegate 플로우는 자연스럽습니다.
다만 Line 90의 TODO는 이미 reportId를 생성자 인자로 전달하고 있어 다소 중복·모호해 보이니, 최종적으로 id만 넘길지/엔티티 전체를 넘길지 결정된 뒤 TODO를 제거하거나 더 구체적인 FIXME 또는 이슈 링크로 정리해 두면 좋겠습니다.
아울러 ReportDetailViewModel 의존성이 Domain 쪽 DIAssembler에 정상 등록돼 있는지만 한 번만 확인 부탁드립니다.

Projects/Presentation/Sources/Home/View/HomeViewController.swift (1)

667-677: Report 제보 플로우의 ViewModel/문구 불일치

ReportRegistrationViewModel을 resolve 해서 ReportRegistrationViewController로 push 하는 구성은 새 제보 플로우와 일관됩니다.
다만 fatalError 메시지에는 "reportRegistrationViewController 의존성"이라고 되어 있어 실제 resolve하는 타입(ViewModel)과 이름이 맞지 않으니, "reportRegistrationViewModel 의존성이 등록되지 않았습니다."처럼 문구를 맞춰 두면 디버깅 시 더 명확할 것 같습니다.

Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (1)

8-38: ReportUseCaseProtocol 메서드 시그니처/주석 정합성 양호

fetchReports / fetchReport / report(title:content:category:location:photos:) 추가가 실제 ReportUseCase 구현과 잘 맞고, 사진 업로드까지 포함한 도메인 요구사항이 명확히 드러납니다.
선택 사항이지만, fetchCurrentLocation·fetchReport처럼 nil을 반환할 수 있는 메서드에 대해 주석에 "조건 불충족 시 nil" 정도만 추가해 두면 호출 측에서 API를 이해하기 조금 더 수월할 것 같습니다.

Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift (1)

10-63: S3Endpoint baseURL/path 및 헤더 조건 한 번만 확인 필요

presigned URL을 그대로 path로 넘기고 baseURL을 빈 문자열로 두는 구현은, 네트워크 레이어에서 baseURL·path를 어떻게 조합하는지에 따라 동작이 달라질 수 있습니다. 만약 URL(string: baseURL + path)처럼 단순 문자열 결합이면 문제 없지만, appendingPathComponent를 사용하는 경우 전체 URL이 잘못 조합될 수 있으니 NetworkService 구현과 함께 한 번만 확인해 주세요.

또한 presigned URL을 생성할 때 Content-Type 등 특정 헤더를 서명에 포함시키는지에 따라, 여기서 headers를 빈 딕셔너리로 두면 업로드가 실패할 가능성도 있습니다. 백엔드와 프로토콜을 맞춰 필요하다면 예를 들어 "Content-Type": "image/jpeg" 같은 헤더를 추가해 두는 편이 안전합니다.

Projects/Domain/Sources/Entity/ReportEntity.swift (1)

19-39: thumbnailURL / photoURLs 필드 추가는 적절하나 이름 일관성 제안

도메인 엔티티에 thumbnailURLphotoURLs를 추가하고 id를 Optional로 바꾼 변경은 상위 레이어(예: 히스토리/상세 뷰모델)에서의 사용 방식과 잘 맞습니다.
다만 이니셜라이저의 파라미터 이름이 photoUrls이고 프로퍼티는 photoURLs라서 대소문자 규칙이 달라 약간 혼동될 수 있습니다. 아래처럼 파라미터 이름도 photoURLs로 맞추면 더 일관될 것 같습니다.

-        thumbnailURL: String?,
-        photoUrls: [String]
+        thumbnailURL: String?,
+        photoURLs: [String]
@@
-        self.thumbnailURL = thumbnailURL
-        self.photoURLs = photoUrls
+        self.thumbnailURL = thumbnailURL
+        self.photoURLs = photoURLs
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)

23-33: 검증/완료 Publisher의 Subject 설계 한 번만 점검해 보면 좋겠습니다

isReportValidPassthroughSubject로 두면, 구독 시점 이후 이벤트만 전달돼서 중간에 새로 구독하는 쪽은 현재 상태를 못 받을 수 있습니다. 뷰모델 라이프사이클 상 항상 뷰와 함께 생성·바인딩된다면 문제는 없지만, 혹시 재바인딩/재구독 가능성이 있다면 CurrentValueSubject<Bool, Never>로 바꿔 두는 편이 더 안전합니다. 반면 등록 완료 이벤트(reportRegistrationCompleteSubject)는 일회성 이벤트라 PassthroughSubject 선택이 적절해 보입니다.

Also applies to: 37-44, 52-61


134-172: 검증 로직과 register()의 guard 조건을 한 곳으로 모아 두면 유지보수가 더 수월할 것 같습니다

verifyIsReportValid()register()guard가 거의 동일한 조건을 중복해서 가지고 있습니다. 현재는 verifyIsReportValid 쪽만 lat/long까지 확인하고 있어, 나중에 요구사항이 바뀌면 두 곳을 동시에 수정해야 합니다.

예를 들어 “필수값이 모두 채워졌는지 판단하는 private computed property”를 두고, 그 값을 isReportValid 발행과 register()의 guard 양쪽에서 참조하도록 통합하면 조건 누락/불일치 위험이 줄어듭니다.

Also applies to: 174-190

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

47-49: 내용 글자 수 카운트 레이블의 최대 글자 수(150)를 상수로 분리해 두는 것이 좋아 보입니다

"150"이 레이블 텍스트("\(content.count) / 150"), UI/기획 스펙 양쪽에서 모두 쓰이는 매직 넘버라서, 예를 들어 Layout 내부나 별도 상수(static let maxContentLength = 150)로 정의해 두고 동일 값을 참조하면 나중에 정책이 바뀔 때 한 군데만 수정하면 되어 유지보수성이 좋아질 것 같습니다.

Also applies to: 67-69, 127-129, 284-294, 371-378

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

204-235: bind()에서 summaryStackView 구성은 한 번만 만들고, 이후에는 값만 갱신하는 구조가 더 안전합니다

현재는 reportDetailPublisher가 emit될 때마다 ReportCompleteContent.allCases를 순회하며 makeContentView 결과를 summaryStackView에 계속 추가하고 있습니다. photoStackView는 매번 비워 주고 있지만, summaryStackView는 정리 없이 늘어날 수 있어(emit 1회라는 보장이 깨질 경우) 중복 뷰가 쌓일 여지가 있습니다.

  • configureLayout() 혹은 configureAttribute()에서 ReportCompleteContent에 대한 컨텐츠 뷰를 한 번만 만들고 stack에 추가한 뒤,
  • bind()에서는 각각의 레이블/이미지뷰에 값만 채우는 방식

으로 분리하면, emit 횟수와 관계없이 UI 구조가 안정적으로 유지됩니다.

Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (2)

34-35: 진행 상태(progress) 필터가 이제 항상 하나만 선택되는 구조인데, 토글 해제 UX가 필요하지 않은지 확인이 필요합니다

filterProgress에서 선택된 progress만 isSelected = true로 만들고, selectedProgress를 항상 해당 값으로 갱신한 뒤 filterReports()에서는

if let selectedProgress,
   selectedProgress != .entire {
    filteredReports = filteredReports.filter { $0.progress == selectedProgress }
}

로 동작하게 되어 있어, 한 번 progress를 누르면 다시 눌러도 해제(=전체로 돌아감)는 되지 않고, 대신 “전체” 칩을 눌러야만 전체 보기로 돌아가게 됩니다.

기획/UX가 “항상 하나의 진행 상태가 선택된 상태를 유지한다”는 의도라면 현재 구현이 적절해 보이고,
이전처럼 같은 항목을 다시 탭하면 필터를 해제하는 토글 UX를 원한다면, 동일 progress를 다시 선택했을 때 selectedProgressnil 또는 .entire로 되돌리는 로직이 필요합니다.

Also applies to: 85-100, 102-117


119-163: fetchReports()의 맵핑/카운트 로직은 도메인 변경에 잘 맞게 정리된 것 같습니다

  • reportEntity.idguard let으로 필터링하고,
  • 날짜 문자열을 Date.convertToDateconvertToString으로 변환한 뒤,
  • thumbnailURLprogress를 그대로 반영해 ReportHistoryItem을 만드는 구조가 도메인 계층 변경과 일관되게 맞춰져 있습니다.
    또한, 이후에 다시 ReportProgress.allCases를 순회하면서 전체/각 상태별 count를 재계산해 progressSubject에 흘려주는 부분도 자연스럽습니다.

별다른 버그 포인트는 안 보이고, 필요하다면 나중에 이 맵핑 로직을 별도 mapper로 분리하는 정도만 고려해 보면 될 것 같습니다.

📜 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 abe4b88 and a25dd6f.

⛔ Files ignored due to path filters (6)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/bitnagil_camera_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/bitnagil_photo_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/[email protected] is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/[email protected] is excluded by !**/*.png
📒 Files selected for processing (37)
  • Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1 hunks)
  • Projects/DataSource/Sources/Common/Enum/Endpoint.swift (2 hunks)
  • Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/FilePresignedDTO.swift (1 hunks)
  • Projects/DataSource/Sources/DTO/ReportDTO.swift (2 hunks)
  • Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift (3 hunks)
  • Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1 hunks)
  • Projects/DataSource/Sources/NetworkService/NetworkService.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/FileRepository.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/ReportRepository.swift (1 hunks)
  • Projects/Domain/Sources/DomainDependencyAssembler.swift (1 hunks)
  • Projects/Domain/Sources/Entity/Enum/ReportType.swift (1 hunks)
  • Projects/Domain/Sources/Entity/ReportEntity.swift (2 hunks)
  • Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (5 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift (1 hunks)
  • Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift (1 hunks)
  • Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (3 hunks)
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (5 hunks)
  • Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (13 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (2 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (4 hunks)
  • Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (5 hunks)
  • Projects/Shared/Sources/Extension/Date+.swift (2 hunks)
✅ Files skipped from review due to trivial changes (4)
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_camera_icon.imageset/Contents.json
  • Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift
  • Projects/Presentation/Resources/Images.xcassets/Report/bitnagil_photo_icon.imageset/Contents.json
  • Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-11-22T11:34:16.094Z
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 73
File: Projects/DataSource/Sources/DTO/FilePresignedDTO.swift:8-8
Timestamp: 2025-11-22T11:34:16.094Z
Learning: In the Bitnagil-iOS project, DTOs in the DataSource module like FilePresignedDTO are intentionally kept internal (without public access modifier) when they are only used within the DataSource module and not exposed to other modules.

Applied to files:

  • Projects/DataSource/Sources/DTO/FilePresignedDTO.swift
  • Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift
📚 Learning: 2025-07-27T10:05:46.043Z
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 25
File: Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift:104-111
Timestamp: 2025-07-27T10:05:46.043Z
Learning: RoutineCreationInputView에서 textField(_:shouldChangeCharactersIn:replacementString:) 메서드가 변경 전 텍스트를 delegate에 전달하는 이유: 변경된 텍스트를 전달하면 텍스트 입력 중에 다른 UI 액션(예: 서브루틴 추가)이 발생할 때 입력 중이던 텍스트가 손실되는 문제가 발생하기 때문. 이는 의도적인 구현으로 동시성 문제를 방지함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift
📚 Learning: 2025-11-19T13:49:08.326Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 68
File: Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift:108-172
Timestamp: 2025-11-19T13:49:08.326Z
Learning: ReportCompleteViewController에서 backgroudView가 fomoImageView와 의도적으로 겹치도록 설계됨. 이를 위해 backgroudView의 top 제약을 fomoImageView.snp.top (또는 fomoImageView)을 기준으로 설정하여 오프셋을 적용함.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
📚 Learning: 2025-07-16T09:09:13.869Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
Repo: YAPP-Github/Bitnagil-iOS PR: 19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Applied to files:

  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
🧬 Code graph analysis (17)
Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/Domain/Sources/DomainDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift (2)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-80)
Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
  • bitnagilChevronIcon (20-22)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (1)
  • reportTextViewDidChanged (520-528)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
Projects/DataSource/Sources/Repository/ReportRepository.swift (3)
  • fetchReports (41-53)
  • fetchReportDetail (55-61)
  • report (14-39)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
Projects/DataSource/Sources/NetworkService/Extension/URLRequest+.swift (1)
  • makeBodyParameter (33-36)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Home/View/HomeViewController.swift (2)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (84-95)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (2)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (84-95)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
  • action (64-81)
  • register (134-172)
Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
  • fetchPresignedURL (14-19)
Projects/DataSource/Sources/Repository/ReportRepository.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-80)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift (4)
Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift (3)
  • viewDidLoad (71-74)
  • configureAttribute (76-95)
  • bind (138-145)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (2)
  • action (32-37)
  • fetchReportDetail (39-61)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
  • attributedString (50-67)
Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (3)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (4)
  • fetchCurrentLocation (25-29)
  • fetchReports (31-33)
  • fetchReport (35-37)
  • report (39-80)
Projects/DataSource/Sources/Repository/ReportRepository.swift (2)
  • fetchReports (41-53)
  • report (14-39)
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (1)
  • fetchReports (119-167)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
  • fetchCurrentLocation (25-29)
  • report (39-80)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
🪛 SwiftLint (0.57.0)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

[Warning] 50-50: TODOs should be resolved (- 사진 업로드 실패 시 에러 처리 필요)

(todo)

Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift

[Warning] 90-90: TODOs should be resolved (- reportCompleteViewController...)

(todo)

🔇 Additional comments (25)
Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (2)

29-30: 쉐브론 아이콘 업데이트가 적절합니다.

디자인 시스템 개선으로 아이콘 크기와 에셋 참조가 업데이트되었습니다. 변경사항이 명확하고 구현이 정확합니다.

Also applies to: 59-59


161-161: 실시간 텍스트 업데이트로 인한 중복 호출 및 성능 검증이 필요합니다.

검증 결과, textViewDidChange(Line 161)와 textViewDidEndEditing(Line 166)에서 동일한 delegate 메서드가 호출됩니다. 사용자가 편집을 종료할 때 마지막 입력으로 인한 호출(Line 161) + 편집 종료 호출(Line 166)이 중복 발생합니다.

또한 ReportRegistrationViewModelconfigureTitle/configureContent 메서드에서 매 입력마다 verifyIsReportValid()를 호출하고 있습니다. 다음을 확인해주세요:

  1. 중복 호출 의도 확인: 두 시점의 호출이 모두 필요한지, 아니면 하나로 통합해야 하는지 검토
  2. 성능 영향 평가: verifyIsReportValid() 구현에서 무거운 작업(유효성 검사, 네트워크 호출 등)이 있는 경우 디바운싱 적용 고려
  3. 팀 정책 확인: 다른 입력 뷰와의 일관성 검토 (예: RoutineCreationInputView의 패턴과 비교)
Projects/Shared/Sources/Extension/Date+.swift (1)

35-36: 새 DateType 포맷 추가 적절합니다

yearMonthDateWeek, yearMonthDateWeek2 포맷이 기존 패턴(yyyy-MM-dd, yy.MM.dd)과 잘 맞고, ko_KR 로케일에서 요일을 짧게 보여주기에도 무난해 보입니다.
해당 케이스를 사용하는 ViewModel 쪽에서 문자열 파싱(convertToDate)까지 필요하다면, 역변환에 사용되는 포맷도 동일한지 정도만 한 번 더 점검해 주세요.

Also applies to: 49-50

Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1)

8-9: SelectableItemicon 추가 설계 괜찮습니다

프로토콜에 icon: UIImage? 를 추가하면서, 기본 구현에서 nil 을 반환하도록 해서 기존 conformer 들이 깨지지 않는 점이 좋습니다.
이 프로토콜이 Presentation 모듈에만 쓰인다면 UIKit 의존성도 수용 가능한 수준이라 보입니다. 다른 모듈(예: Domain)로 확장할 계획이 있다면, 그때는 아이콘 정보를 별도의 value type 으로 추상화하는 정도만 염두에 두면 될 것 같습니다.

Also applies to: 14-19

Projects/DataSource/Sources/Common/Enum/Endpoint.swift (1)

8-9: Endpoint의 bodyType/bodyData 확장은 방향성 좋습니다

JSON 기반 기존 엔드포인트는 bodyType = .json, bodyParameters 만 사용하고, 파일 업로드 등은 bodyType = .rawData, bodyData 를 사용하는 구조로 잘 분리된 것 같습니다.
다만 Endpoint+.swift 에서 bodyType 에 따라 httpBody 생성 시 bodyParametersbodyData 가 동시에 사용되지 않도록(하나는 반드시 무시되도록) 분기 처리가 되어 있는지만 한 번만 확인해 주세요.

Also applies to: 18-25

Projects/DataSource/Sources/DTO/ReportDTO.swift (4)

10-10: LGTM!

Codable 채택이 적절합니다. ReportDTO가 이제 네트워크 요청 본문(인코딩)과 응답(디코딩) 모두에 사용되므로, Decodable에서 Codable로의 변경이 타당합니다.


16-16: LGTM!

reportStatus를 옵셔널로 변경하고 안전한 기본값 매핑을 사용하는 것이 적절합니다. 이중 폴백 패턴(?? "" ?? .received)이 nil 또는 유효하지 않은 값을 안전하게 처리합니다.

Also applies to: 29-29, 46-46


35-35: LGTM!

reportImageUrlthumbnailURL로 매핑하는 것이 적절하며, 도메인 엔티티의 업데이트와 일관성을 유지합니다.

Also applies to: 52-52


23-37: reportId guard 불일치는 의도된 설계 패턴입니다.

ReportEntity의 id 필드가 Int?(선택형)로 정의되어 있으므로, 두 메서드의 서로 다른 guard 처리는 의도적입니다:

  • toReportEntity(): reportId를 guard하지 않음 → 새 리포트(서버가 아직 ID를 할당하지 않은 경우)용
  • toReportEntity(date:): guard let reportId 유지 → 기존 리포트(이미 서버에서 ID를 할당받은 경우)용

실제 사용 패턴도 이를 확인해줍니다:

  • 단일 리포트 fetch (ReportRepository:60): 새로운 리포트 처리
  • 그룹화된 과거 리포트 (ReportRepository:48): 기존 리포트 처리

일관성 있는 설계이므로 추가 조치가 필요하지 않습니다.

Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift (1)

49-50: LGTM!

셀 선택 스타일을 비활성화하여 탭 시 하이라이트가 표시되지 않도록 합니다. 이는 적절한 UI 개선입니다.

Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)

52-54: LGTM!

FileRepositoryProtocol에 대한 DI 등록이 적절합니다. 기존 등록 패턴과 일관성을 유지하며, 새로운 파일 업로드 기능을 지원합니다.

Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1)

8-11: LGTM!

FilePresignedConditionDTO 구조가 적절합니다. 프리사인드 URL 요청 조건을 위한 간단하고 명확한 DTO이며, Codable 채택이 올바릅니다. Based on learnings, DataSource 모듈 내에서만 사용되는 DTO는 의도적으로 internal로 유지됩니다.

Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)

81-83: LGTM!

EmptyResponseDTO에 대한 특수 케이스 처리가 적절합니다. 빈 응답(예: 204 No Content)을 반환하는 엔드포인트를 올바르게 처리하며, 기존 디코딩 로직에 영향을 주지 않습니다.

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

10-10: LGTM!

Foundation import 추가가 적절합니다. Date 타입 사용을 위해 필요합니다.


53-53: LGTM!

photoUrls에서 photoURLs로 변경이 도메인 엔티티 업데이트와 일치합니다.


43-47: 날짜 포맷 변환 로직 검증 완료

검증 결과:

  • yearMonthDateyearMonthDateWeek2 모두 Date+.swift에 정의됨 (33번, 36번 줄)
  • ✅ 형식 정의가 일치: yearMonthDate = "yyyy-MM-dd"yearMonthDateWeek2 = "yyyy-MM-dd (E)"
  • reportEntity.date가 nil이거나 잘못된 형식일 경우 빈 문자열로 폴백되는 것이 의도된 동작이며, 옵셔널 체이닝과 nil 병합 연산자로 안전하게 처리됨

로직이 정확하고 모든 요구사항이 충족되었습니다.

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

270-270: LGTM!

$0.date 직접 사용이 적절합니다. ReportHistoryItem.date가 이제 non-optional이 되어 ?? "" 폴백이 더 이상 필요하지 않습니다. 이는 날짜 처리 개선과 일관성을 유지합니다.


282-282: 스냅샷 애니메이션 비활성화를 확인해주세요.

animatingDifferencestrue에서 false로 변경되었습니다. 이것이 의도된 것인지 확인해주세요:

  • 성능 최적화를 위한 것인가요?
  • 애니메이션 중 시각적 버그를 방지하기 위한 것인가요?

변경 이유가 명확하지 않다면, 사용자 경험 관점에서 애니메이션을 유지하는 것을 고려해보세요.

Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)

15-22: 검증 완료 - 모든 엔드포인트가 정상적으로 작동합니다.

검증 결과, bodyType 기반 요청 본문 처리 로직은 완벽하게 구현되었습니다:

  • 기본 구현: Endpoint 프로토콜의 확장에서 bodyType: .json, bodyData: nil 기본값 제공으로 8개 엔드포인트가 안전하게 작동
  • rawData 엔드포인트: FilePresignedEndpoint, S3Endpoint 모두 bodyData를 명시적으로 구현하여 올바르게 동작
  • 안전한 처리: switch 문에서 if let data = bodyData 옵셀 체크로 rawData 경우를 안전하게 처리

기존 엔드포인트 모두 호환성을 유지합니다.

Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)

31-37: 리포트 조회 관련 메서드는 단순 위임으로 무난합니다

fetchReportsfetchReport(reportId:)가 각각 ReportRepositoryProtocol에 그대로 위임하는 구조는 계층 역할 분리에 잘 맞고, 예외 처리도 상위로 투명하게 전파돼 자연스럽습니다. 별도 수정 필요해 보이지 않습니다.

Projects/DataSource/Sources/DTO/FilePresignedDTO.swift (1)

8-18: FilePresignedDTO는 사용되지 않는 미사용 코드이므로 제거해야 합니다

검색 결과 FilePresignedDTO는 정의되었을 뿐 코드베이스의 어떤 곳에서도 실제로 사용되지 않고 있습니다. 헤더 주석 외에는 참조가 전혀 없으므로 이 구조체는 제거하는 것이 좋겠습니다.

Projects/DataSource/Sources/DTO/FilePresignedDTO.swift 파일 자체를 삭제하거나, 추후 실제 사용할 계획이 있다면 Repository나 Endpoint 계층에서 이 DTO를 활용하도록 연결해 주시기 바랍니다.

⛔ Skipped due to learnings
Learnt from: taipaise
Repo: YAPP-Github/Bitnagil-iOS PR: 73
File: Projects/DataSource/Sources/DTO/FilePresignedDTO.swift:8-8
Timestamp: 2025-11-22T11:34:16.094Z
Learning: In the Bitnagil-iOS project, DTOs in the DataSource module like FilePresignedDTO are intentionally kept internal (without public access modifier) when they are only used within the DataSource module and not exposed to other modules.
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)

8-8: Shared 모듈 import 추가 적절

Bitnagil* 관련 심볼과 DIContainer를 사용하는 파일이라 Shared import 추가가 필요해 보입니다. 별도 이슈 없습니다.

Projects/Domain/Sources/DomainDependencyAssembler.swift (1)

66-77: ReportUseCase DI에 FileRepository 의존성 추가 OK

locationRepository / reportRepository / fileRepository를 한 번에 resolve 해서 ReportUseCase를 주입하는 구성이 현재 UseCase 초기화 시그니처와 잘 맞습니다. 다만 상위 assembler에서 FileRepositoryProtocol 등록이 누락돼 있으면 여기서 fatalError로 앱이 종료되므로, 해당 등록이 포함돼 있는지만 한 번 확인해 주세요.

Projects/DataSource/Sources/Repository/FileRepository.swift (1)

11-24: FileRepository 구현이 presigned URL + 업로드 플로우와 잘 맞습니다

fetchPresignedURL에서 조건 DTO 배열을 만들어 한 번에 presigned URL을 받아오고, uploadFile에서 S3Endpoint.uploadImage를 통해 rawData PUT 요청을 보내는 구조가 간결하고, 도메인 계층에서 기대하는 FileRepositoryProtocol 인터페이스와도 잘 맞습니다. 현재 수준에서는 별도 수정 없이도 충분해 보입니다.

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

284-293: makePhotoView() 구현은 역할 분리가 잘 되어 있습니다

사진 썸네일용 뷰를 별도 팩토리 메서드로 빼고, 크기/코너 라운드/배경색을 한 곳에서 관리하는 구조가 깔끔합니다. 이후 다른 화면에서 동일 스타일 썸네일이 필요해지면 이 메서드를 재사용할 수 있을 것 같습니다.

Comment on lines +29 to +33
var method: HTTPMethod {
switch self {
case .fetchPresignedURL: .post
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

method 프로퍼티에서 return 누락으로 컴파일 에러가 발생합니다

switch 내부에서 .post를 리턴하지 않아 현재 코드는 빌드되지 않습니다. 아래처럼 return을 명시해야 합니다.

    var method: HTTPMethod {
        switch self {
-        case .fetchPresignedURL: .post
+        case .fetchPresignedURL:
+            return .post
        }
    }

추가로, 이 케이스가 하나뿐이라면 switch 대신 단순히 return .post로 구현해도 충분합니다.

🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift around lines
29 to 33, the computed property `method` currently uses a `switch` but doesn't
return the `.post` value, causing a compile error; fix it by either adding an
explicit `return .post` inside the `case .fetchPresignedURL:` branch or simplify
the implementation to directly `return .post` (preferred if this enum has only
that single case), ensuring the property always returns an HTTPMethod.

Comment on lines +12 to +26
/// 제보를 등록합니다.
/// - Parameters:
/// - title: 제보 제목
/// - content: 제보 내용
/// - category: 제보 카테고리
/// - location: 제보 위치
/// - photos: 업로드한 사진의 presigned urls
/// - Returns: 제보 id
func report(
title: String,
content: String?,
category: ReportType,
location: LocationEntity?,
photoURLs: [String]
) async throws -> Int?
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

report 파라미터 주석이 시그니처 및 실제 값 의미와 맞지 않습니다

현재 주석과 구현 사이에 두 가지 작은 불일치가 있습니다.

  • 파라미터 이름은 photoURLs인데, 주석에는 photos라고 되어 있음
  • 주석에는 “presigned urls”라고 되어 있지만, 실제로 ReportUseCase에서는 publicImageURLs(query 스트립된 공개 URL)을 넘기고 있어, presigned URL이라 보기 어렵습니다

혼동을 줄이기 위해 주석을 아래처럼 맞춰 주는 것을 제안드립니다.

-    ///   - photos: 업로드한 사진의 presigned urls
+    ///   - photoURLs: 업로드가 완료된 사진의 공개 URL 목록
     /// - Returns: 제보 id
     func report(
         title: String,
         content: String?,
         category: ReportType,
         location: LocationEntity?,
-        photoURLs: [String]
+        photoURLs: [String]
     ) async throws -> Int?

이렇게 해 두면, 호출 측에서도 “파일 업로드까지 끝난 뒤 최종적으로 저장되는 URL을 넘긴다”는 의도가 더 명확해질 것 같습니다.

🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift
around lines 12 to 26, the parameter doc is inconsistent with the signature and
usage: update the @param for photoURLs (replace "photos") and remove "presigned
urls", describing that the method expects final public image URLs
(query-stripped) as used by ReportUseCase; adjust the brief so callers know
these are the final stored/public URLs passed after upload.

Comment on lines +39 to +79
public func report(
title: String,
content: String?,
category: ReportType,
location: LocationEntity?,
photos: [Data]
) async throws -> Int? {
if photos.isEmpty { return nil }

let fileNames = (1...photos.count).map { "\($0).jpg" }

// TODO: - 사진 업로드 실패 시 에러 처리 필요
guard
let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
presignedDict.count == photos.count
else { return nil }

let presignedURLs = fileNames.compactMap { fileName in
presignedDict[fileName]
}

for (url, photo) in zip(presignedURLs, photos) {
do {
try await fileRepository.uploadFile(url: url, data: photo)
} catch {
print(error.localizedDescription)
}
}

let publicImageURLs = presignedURLs.map { url in
url.split(separator: "?", maxSplits: 1)
.map(String.init)
.first ?? url
}

return try await reportRepository.report(
title: title,
content: content,
category: category,
location: location,
photoURLs: publicImageURLs)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

사진 업로드 실패 시 조용히 진행되는 현재 구현은 정합성 문제가 될 수 있습니다

report(...) 내부에서 사진 업로드 플로우가 다음과 같이 동작하는 점이 조금 위험해 보입니다.

  • fetchPresignedURL 실패/개수 불일치 시에는 nil 반환으로 조용히 종료
  • 업로드 루프에서 각 uploadFile 에러를 print만 하고 무시한 뒤,
    실패 여부와 상관없이 reportRepository.report(...)를 호출
  • 결과적으로 서버에는 실제로 업로드되지 않은 이미지 URL이 포함된 제보가 등록될 수 있음

실제 서비스 관점에서는 “사진이 일부/전부 업로드되지 않았는데도 성공처럼 보이는 제보”가 생길 수 있어, 최소한 전체 업로드 실패 시에는 에러를 throw 하거나, 업로드된 것만 필터링해서 전달하는 등의 처리가 필요해 보입니다. SwiftLint도 TODO를 경고하고 있으니 이 부분을 이번 PR에서 함께 정리하는 것이 좋겠습니다.

한 가지 단순한 개선안은 “업로드 실패 시 전체 제보 등록을 중단하고 에러를 전파”하는 것입니다.

-        // TODO: - 사진 업로드 실패 시 에러 처리 필요
-        guard
-            let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
-            presignedDict.count == photos.count
-        else { return nil }
+        guard
+            let presignedDict = try await fileRepository.fetchPresignedURL(prefix: "report", fileNames: fileNames),
+            presignedDict.count == photos.count
+        else {
+            // presigned URL 개수 불일치는 로직/서버 문제에 가까우므로 에러로 보고합니다.
+            throw ReportError.invalidPresignedURLCount
+        }
@@
-        for (url, photo) in zip(presignedURLs, photos) {
-            do {
-                try await fileRepository.uploadFile(url: url, data: photo)
-            } catch {
-                print(error.localizedDescription)
-            }
-        }
+        for (url, photo) in zip(presignedURLs, photos) {
+            // 업로드 실패 시 바로 throw 되어 전체 제보 등록을 중단합니다.
+            try await fileRepository.uploadFile(url: url, data: photo)
+        }

위 예시의 ReportError.invalidPresignedURLCount는 도메인 에러 enum 등을 새로 정의해야 하므로, 실제 구현에서는 프로젝트에서 이미 사용 중인 에러 타입에 맞춰 조정해 주시면 됩니다.

추가로:

  • photos.isEmpty { return nil } 도 “사진이 필수인지 여부”에 따라 throw로 validation 실패를 알려주는 쪽이 더 디버깅에 유리할 수 있습니다.
  • presignedURLs.count == photos.count 여부를 한 번 더 guard로 확인해 두면, key mismatch 시 일부 사진만 업로드되는 상황도 방지할 수 있습니다.

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

🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 50-50: TODOs should be resolved (- 사진 업로드 실패 시 에러 처리 필요)

(todo)

Comment on lines 99 to 110
private func configureLocation() {
Task {
do {
self.location = try await reportUseCase.fetchCurrentLocation()
locationSubject.send(location?.address)
verifyIsReportValid()
} catch {

locationSubject.send(nil)
verifyIsReportValid()
}

locationSubject.send(location?.address)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

위치 조회 실패 시 location 프로퍼티가 초기화되지 않아 검증/실제 등록 상태가 어긋날 수 있습니다

configureLocation()catch 블록에서 locationSubjectnil을 보내지만, self.location은 그대로 유지됩니다. 이 상태에서 verifyIsReportValid()는 이전에 성공했던 위치값을 계속 보고 true를 줄 수 있고, register()도 이전 위치로 신고를 보냅니다. UI에는 위치가 비어 보이는데 실제로는 이전 좌표로 신고되는 상황이 생길 수 있습니다.

에러 시 self.location도 함께 nil로 초기화하는 쪽이 더 일관될 것 같습니다.

    private func configureLocation() {
        Task {
            do {
                self.location = try await reportUseCase.fetchCurrentLocation()
                locationSubject.send(location?.address)
                verifyIsReportValid()
            } catch {
-               locationSubject.send(nil)
-               verifyIsReportValid()
+               self.location = nil
+               locationSubject.send(nil)
+               verifyIsReportValid()
            }
        }
    }

또한 검증 로직(verifyIsReportValid)에서 lat/long까지 체크해 주고 있으니, 이 상태라면 등록/버튼 활성 조건 모두 동일하게 맞춰집니다.

Also applies to: 174-190

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
around lines 99-110 (and similarly 174-190), the catch path for
configureLocation() only sends nil to locationSubject but leaves self.location
unchanged, causing validation and registration to use stale coordinates; update
the catch block(s) to also set self.location = nil before sending
locationSubject.send(nil) and calling verifyIsReportValid(), and ensure
verifyIsReportValid() validates presence of latitude/longitude (not just non-nil
location) so button enablement and register() use the same lat/long checks.

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.

3 participants