⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@taipaise
Copy link
Collaborator

@taipaise taipaise commented Nov 22, 2025

🌁 Background

  • 제보하기 관련 기능을 거의~ 완성했습니다!

👩‍💻 Contents

  • 제보하기 화면 디자인 확정 사항 적용 c00b652)
  • 제보하기 로직 구현 (898e3e5, 4239784)
  • 제보 히스토리 필터링 로직 구현 및 미비사항 보완 (064990b, c5f69a3)

📝 Review Note

NetworkService로직 변경

  • S3로 이미지를 업로드 하거나, 일부 api 의 경우 (presignedurl 발급 api) body에 json 형식이 아닌 rawdata를 바로 넣거나, 기존 구조를 가져가기 어려운 문제가 있었습니다.

  • 기존에는 정의한 Endpoint를 기반으로 URLRequest의 extension에서 httpBody를 만들고 있었습니다. (아래 코드 참고)

    extension URLRequest {
        // 생략
        mutating func makeBodyParameter(with parameters: [String: Any]) throws {
            if parameters.isEmpty { return }
            httpBody = try JSONSerialization.data(withJSONObject: parameters)
        }
    }

이를 해결하기 위해, Http에 들어갈 body의 형식을 rawdata로 지정할 경우, 따로 makeBodyParameter 메서드에서 body를 만들지 않고, endpoint에서 설정한 body를 사용하도록 수정했습니다.

public enum EndpointBodyType {
    case json
    case rawData
}

public protocol Endpoint {
    var baseURL: String { get }
    var path: String { get }
    var method: HTTPMethod { get }
    var headers: [String: String] { get }
    var queryParameters: [String: String] { get }
    var bodyParameters: [String: Any] { get }
    var isAuthorized: Bool { get }
    var bodyType: EndpointBodyType { get }
    var bodyData: Data? { get }
}

extension Endpoint {
    var bodyType: EndpointBodyType { return .json }
    var bodyData: Data? { return nil }
}
  • Endpoint의 extension에서 bodyType과 bodyData의 기본값을 지정한 이유는, 기존 코드의 수정을 최소화하기 위함입니다.
  • 사용예시는 S3Endpoint.swift 를 참고해주시면 감사하겠습니다!

제보 엔티티 구조 변경

  • ReportEntity 의 id를 Int에서 Int? 로 변경하였습니다.
  • 예상하지 못한 부분이었는데요, 제보 상세 api에서 응답 값으로 넘어온 json에 제보 id가 포함되어 있지 않았습니다. 또한 기존에는 DTO -> Entity 변경 시 id가 없으면 decodingErrorthrow 하는 방식으로 구현되어 있었습니다. 때문에 항상 toEntity() 가 실패하여 제보 상세 화면에 표시할 제보 내용이 없는 문제가 있었습니다.
  • 때문에 우선 id를 옵셔널 값이 될 수 있도록 수정했습니다.
  • photoURL 의 프로퍼티 명을 좀 더 직관적으로 thumbnailURL로 수정하였습니다.

제보하기 완료 이후 플로우 (논의 필요)

  • 제보하기에 필요한 정보들을 모은 후에, 이걸 어떻게 처리할 지에 대해 고민을 많이 하고 있습니다. 물론 로딩 화면이 있는 이상, 조이 말씀대로 로딩 화면에서 api를 쏘는 것이 적절하다 생각했습니다.
  • 다만 그러기 위해서는 제보하기 -> 로딩 화면으로 제보 내용을 전달해줘야 합니다. 그리고 여기에는 항상 고민의 원인이 되는 용량이 큰 이미지들 이 있습니다.
  • 또한 기존의 entity를 사용하여 데이터를 깔끔하게 옮길 수 없다는 문제도 있었습니다. 기존 제보 entity는 이미지를 Data 형식이 아닌 url 형식으로 들고 있기 때문입니다.
  • 우선은 아래와 같은 플로우로 구현을 진행했습니다.
    • 제보하기 버튼을 누르면, 로딩 화면을 보여준다. (이때 로딩 화면은 제보VC의 delegate를 위임받는다)

    • 제보하기 버튼을 누르면, 제보ViewModel은 제보하기 로직을 수행한다. (비동기로 진행)

      registerButton.addAction(
                  UIAction { [weak self] _ in
                      let loadingViewController = ReportLoadingViewController()
                      self?.delegate = loadingViewController
                      self?.navigationController?.pushViewController(loadingViewController, animated: true)
                      self?.viewModel.action(input: .register)
                  },
                  for: .touchUpInside)
    • 제보가 완료되면 제보VC는 delegate를 통해 로딩 화면에게 등록한 제보 id를 넘긴다. (이때 최소 0.7초 이후에 전달합니다. 로딩 화면이 너무 화면에 짧게 나타났다 사라지는 것을 막기 위함)

        viewModel.output.reportRegistrationCompletePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] reportId in
                guard let self else { return }
      
                delegate?.reportRegistrationViewController(self, completeRegistration: reportId)
            }
            .store(in: &cancellables)
    • 로딩VC는 제보 id를 완료VC에 넘긴다. 이후 완료VC는 아이디를 바탕으로 제보 내용을 fetch한다. (미구현 상태)

  • 아마 제보하기 -> 로딩 화면으로 객체를 전달해주는 경우 Presentation쪽에 이를 위한 struct 정의가 필요해보입니다. 그리고 해당 객체를 제보 VC -> 로딩 VC -> 완료 VC로 넘기는 형식이 될 것 같습니다.
  • 공수는 크게 들것 같지는 않아서 편하게 결정해도 될 것 같습니다! (제보하기 api자체는 제보하기VC에서 쏘고, 제보 완료시 결과를 로딩VC -> 완료VC 순으로 넘기는 방법)

이외 미비사항

  • 디자인 수정 사항 중에, 카메라나 앨범을 선택하는 bottom sheet에 icon을 표시해야합니다. 이를 위해 SelectableItem 프로토콜을 아래와 같이 수정했습니다.
    protocol SelectableItem {
        var id: Int { get }
        var displayName: String? { get }
        var description: String { get }
        var icon: UIImage? { get }
    }
    
    extension SelectableItem {
        var icon: UIImage? { return nil }
    }
  • 그리고, 카메라/앨범 선택시 사용하는 SelectPhotoType: SelectableItem 열거형에 icon을 따로 정의해줬습니다.
  • SelectableItemCell 에는 유동적으로 icon 이미지를 표시할 수 있게 icon 이미지뷰와, titleLabel을 stackView에 추가해주었습니다. SelectableItemCell.swift
  • 그리고, 이 SelectableItem의 icon이 nil이 아니면 SelectableItemCell에 추가한 iconImageView가 표시되도록 했는데, 원하는대로 동작하지 않고 아이콘이 보이지 않는 문제가 있습니다 ㅠ 아무리 봐도 당장은 해결이 안되어 일단은 넘겨둔 상황입니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 신고 등록 시 사진 업로드 기능 추가
    • 신고 내용 입력 시 글자 수 제한 표시
    • 신고 위치 선택 및 표시 개선
  • UI/UX 개선

    • 사진 선택 시 카메라 및 갤러리 아이콘 추가
    • 선택 항목 셀에 아이콘 표시 기능 추가
    • 신고 목록에 썸네일 이미지 표시
  • 버그 수정

    • 신고 완료 후 홈 화면으로 돌아가는 기능 개선

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

- request 생성로직 수정. body에 jsonData 뿐 아니라, rawdata도 들어갈 수 있도록 수정 (s3 업로드 위함)
- response body가 empty여도 error를 throw 하지 않도록 수정
- 진행상황 collectionView cell에 진행상황 별 갯수 표시
- ReportEntity id 값을 옵셔널로 변경
- ReportDetail 날짜 포멧 변경
@taipaise taipaise requested a review from choijungp November 22, 2025 11:08
@taipaise taipaise self-assigned this Nov 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 22, 2025

개요

파일 업로드 및 보고서 등록 기능을 추가합니다. 새로운 FileRepository와 엔드포인트를 도입하고, ReportDTO와 ReportEntity를 수정하여 선택적 ID와 썸네일 URL을 지원합니다. ReportRegistrationViewController에 위임자 패턴과 유효성 검증을 추가합니다.

변경 사항

응집체 / 파일 변경 요약
파일 저장소 프로토콜 및 구현
Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift, Projects/DataSource/Sources/Repository/FileRepository.swift
사전 서명된 URL 가져오기 및 파일 업로드 메서드를 포함하는 새로운 FileRepositoryProtocol 정의 및 구현. NetworkService를 통해 FilePresignedEndpoint 및 S3UploadEndpoint 호출
파일 엔드포인트 및 DTO
Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift, Projects/DataSource/Sources/Endpoint/S3UploadEndpoint.swift, Projects/DataSource/Sources/DTO/FilePresignedDTO.swift, Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift
사전 서명된 URL 가져오기(rawData 본문) 및 S3 업로드 엔드포인트 추가. 추가 속성 매핑이 있는 FilePresignedDTO 및 조건 DTO 추가
엔드포인트 본문 처리
Projects/DataSource/Sources/Common/Enum/Endpoint.swift, Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift, Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift
EndpointBodyType(json/rawData) 열거형 추가. Endpoint 프로토콜에 bodyType 및 bodyData 속성 추가. makeURLRequest에서 bodyType에 따라 본문 인코딩 처리
보고서 DTO 및 엔티티
Projects/DataSource/Sources/DTO/ReportDTO.swift, Projects/Domain/Sources/Entity/ReportEntity.swift
ReportDTO를 Codable로 변경, reportStatus를 선택적으로 만들고 thumbnailURL 추가. ReportEntity의 id를 선택적으로 만들고 photoURLs 및 thumbnailURL 추가
보고서 저장소 프로토콜 및 리포지토리
Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift, Projects/DataSource/Sources/Repository/ReportRepository.swift, Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift
report() 메서드를 개별 매개변수(제목, 내용, 카테고리, 위치, 사진)를 받는 새 시그니처로 변경. register() 엔드포인트 케이스 추가. Int? 반환 및 throws 추가
보고서 UseCase
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift, Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift
FileRepositoryProtocol 의존성 주입. fetchReports() 및 fetchReport() 메서드 추가. report() 메서드를 매개변수 기반으로 변경하고 사진 업로드 워크플로우 통합
의존성 어셈블러
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift, Projects/Domain/Sources/DomainDependencyAssembler.swift
FileRepositoryProtocol 등록 추가. ReportUseCase에 fileRepository 주입
보고서 등록 UI 및 ViewModel
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift, Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
ReportRegistrationViewControllerDelegate 프로토콜 추가. 콘텐츠 문자 개수 레이블, 제목 레이블 추가. isReportValid 및 reportRegistrationCompletePublisher 출력 추가. 유효성 검증 흐름 구현
보고서 UI 업데이트
Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift, Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift, Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift, Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift
ReportCompleteViewController에 루트로 돌아가기 동작 추가. ReportLoadingViewController에 위임자 구현 추가. ReportHistoryViewController 그룹화 및 스냅샷 애니메이션 조정. ReportTextView에서 chevron 아이콘 변경
선택 가능 항목 및 아이콘
Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift, Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift, Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift, Projects/Presentation/Sources/Report/Model/SelectPhotoType.swift
SelectableItem 프로토콜에 icon 속성 추가. SelectableItemCell에서 아이콘을 표시하는 스택 뷰 추가. bitnagilCameraIcon 및 bitnagilPhotoIcon 추가. SelectPhotoType에서 아이콘 구현
날짜 형식 및 기타
Projects/Shared/Sources/Extension/Date+.swift, Projects/Domain/Sources/Entity/Enum/ReportType.swift, Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift, Projects/Presentation/Sources/Home/View/HomeViewController.swift
날짜 형식 타입 추가(yearMonthDateWeek, yearMonthDateWeek2). ReportType에 CustomStringConvertible 추가. HomeViewController에서 ReportDetailViewController 대신 ReportRegistrationViewController 사용
에셋
Projects/Presentation/Resources/Images.xcassets/Report/*
카메라 및 사진 아이콘 이미지 에셋 추가

시퀀스 다이어그램

sequenceDiagram
    participant User
    participant RegVC as ReportRegistrationViewController
    participant RegVM as ReportRegistrationViewModel
    participant LoadingVC as ReportLoadingViewController
    participant ReportUC as ReportUseCase
    participant FileRepo as FileRepository
    participant ReportRepo as ReportRepository

    User->>RegVC: 등록 시작
    RegVC->>RegVM: register() 호출
    RegVM->>ReportUC: report(title, content, category, location, photos) 호출
    
    rect rgb(200, 220, 255)
    Note over ReportUC: 사진 업로드 워크플로우
    ReportUC->>FileRepo: fetchPresignedURL(prefix, fileNames)
    FileRepo-->>ReportUC: [fileName: presignedURL]
    ReportUC->>FileRepo: uploadFile(url, data) × N
    FileRepo-->>ReportUC: (각 사진 업로드 완료)
    end
    
    rect rgb(220, 255, 220)
    Note over ReportUC: 보고서 제출
    ReportUC->>ReportRepo: report(title, content, ..., photoURLs)
    ReportRepo-->>ReportUC: reportId
    end
    
    ReportUC-->>RegVM: reportId
    RegVM->>RegVC: reportRegistrationCompletePublisher(reportId)
    RegVC->>LoadingVC: 밀어넣기 및 위임자 호출
    LoadingVC->>RegVC: reportRegistrationViewController(..., completeRegistration: reportId)
    RegVC->>RegVC: ReportCompleteViewController 표시
    User->>RegVC: 확인 누르기
    RegVC->>RegVC: 루트로 돌아가기
Loading

예상 코드 리뷰 노력

🎯 4 (복잡함) | ⏱️ ~45분

주요 검토 영역:

  • ReportUseCase의 사진 업로드 워크플로우: 사전 서명된 URL 가져오기와 여러 파일 업로드 조율, 오류 처리 및 최소 지연 로직 검증
  • ReportRegistrationViewController의 위임자 패턴: 프로토콜 구현, 위임자 호출 타이밍, 네비게이션 흐름의 일관성 확인
  • ReportDTO 및 ReportEntity 변경: id 선택성 도입, 새로운 속성 매핑, 기존 코드와의 호환성 검증
  • 엔드포인트 본문 처리: bodyType 조건에 따른 인코딩 로직, rawData 처리의 정확성
  • ReportRegistrationViewModel의 유효성 검증: isReportValid 계산 로직, 상태 변경 시 올바른 발행 확인
  • 의존성 주입: FileRepositoryProtocol 추가에 따른 DI 컨테이너 설정의 일관성

관련 PR

🐰 파일이 날아가고 사진이 올라가고,
보고서는 제목과 내용을 가지고,
위임자는 길을 안내하고,
유효성은 모든 것을 확인해,
드디어 완성이야! 🎉

Pre-merge checks and finishing touches

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

📜 Recent 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 4239784 and 3d08636.

📒 Files selected for processing (5)
  • Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1 hunks)
  • Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1 hunks)
  • Projects/DataSource/Sources/Repository/FileRepository.swift (1 hunks)
  • Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/FilePresignedConditionDTO.swift
🧬 Code graph analysis (4)
Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
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/Endpoint/FilePresignedEndpoint.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
  • fetchPresignedURL (14-19)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
🪛 SwiftLint (0.57.0)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

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

(todo)

⏰ 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 (5)
Projects/DataSource/Sources/DTO/FilePresignedConditionDTO.swift (1)

1-11: 이전 리뷰의 오타가 수정되었습니다!

파일명과 타입명의 오타(FilePresignedConditaionDTOFilePresignedConditionDTO)가 올바르게 수정되었습니다. DTO 구조도 명확하고 Codable 준수도 적절합니다.

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

46-58: 이전 리뷰의 이슈 #1, #2가 해결되었습니다!

빈 배열 검증(if photos.isEmpty)과 명시적인 fileName 기반 URL 매핑이 추가되어 이전 리뷰에서 지적된 두 가지 중요한 문제가 해결되었습니다:

  • Range 크래시 위험 제거
  • presigned URL과 사진의 순서 보장
Projects/Domain/Sources/Protocol/Repository/FileRepositoryProtocol.swift (1)

1-23: 이전 리뷰의 파일명 오타가 수정되었습니다!

파일명이 올바르게 수정되었고, 프로토콜 정의도 명확합니다. 메서드 시그니처와 문서화가 잘 되어 있어 구현체에서 사용하기 좋습니다.

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

11-25: 구현이 명확하고 간결합니다

FileRepositoryProtocol의 구현이 깔끔하며, NetworkService를 통한 에러 처리와 재시도 로직이 일관되게 적용되어 있습니다. DTO 매핑과 엔드포인트 호출 패턴도 다른 레포지토리들과 일관성 있게 작성되었습니다.

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

14-68: 새로운 bodyType/bodyData 패턴이 올바르게 적용되었습니다

엔드포인트 구현이 명확하며, PR 설명에 언급된 새로운 EndpointBodyType (.rawData) 패턴을 올바르게 사용하고 있습니다. bodyParameters가 빈 딕셔너리를 반환하고 실제 페이로드는 bodyData에서 제공하는 것이 새로운 패턴에 맞게 구현되었습니다.


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: 7

🧹 Nitpick comments (17)
Projects/Presentation/Sources/Report/View/Component/ReportHistory/ReportHistoryTableViewCell.swift (1)

47-51: 셀 선택 하이라이트 제거 의도 확인 필요

selectionStyle = .none으로 기본 선택 하이라이트를 완전히 끈 상태입니다.
이 셀이 탭 시 상세 화면 진입 등 인터랙션을 가진다면, 사용자가 탭 여부를 인지하기 어려울 수 있어 UX 측면에서 한 번 더 디자인/기획 의도 확인하는 것이 좋겠습니다. 필요한 경우 selectedBackgroundView 커스텀 등 대체 피드백을 두는 것도 고려해 보세요.

Projects/Shared/Sources/Extension/Date+.swift (1)

32-38: DateType 케이스 추가는 문제 없어 보입니다만, 이름 구분이 조금 더 명확하면 좋겠습니다.

yearMonthDateWeek / yearMonthDateWeek2 둘 다 필요한 상황이라면, 괄호 유무 같은 차이가 코드만 봐서는 잘 드러나지 않습니다.
향후 유지보수를 위해 withParen, compact 등 의미가 드러나는 네이밍이나 간단한 주석으로 의도를 남겨 두는 것을 고려해 주세요.

Projects/Presentation/Sources/Report/View/Component/ReportRegistration/ReportTextView.swift (2)

58-61: bitnagilChevronIcon가 이미 template 이미지를 반환하므로 withRenderingMode(.alwaysTemplate)는 중복입니다

BitnagilIcon.bitnagilChevronIcon(direction:) 쪽에서 이미 .withRenderingMode(.alwaysTemplate)를 적용하고 있어서, 여기서 한 번 더 호출하는 것은 기능상 문제는 없지만 약간 중복입니다.

큰 문제는 아니니, 나중에 리팩터링 시 아래처럼 단순화해도 될 것 같습니다:

- chevronImage.image = BitnagilIcon
-     .bitnagilChevronIcon(direction: .down)?
-     .withRenderingMode(.alwaysTemplate)
+ chevronImage.image = BitnagilIcon
+     .bitnagilChevronIcon(direction: .down)

155-167: textViewDidChange + textViewDidEndEditing 모두에서 delegate를 부르는 동작 의도 확인 필요

이제 textViewDidChange에서 매 키 입력마다, textViewDidEndEditing에서 편집 종료 시 한 번 더 reportTextViewDidChanged(_:text:)가 호출됩니다.

  • ViewModel 액션(.inputTitle, .inputContent)이 가벼운 상태 업데이트라면 문제 없겠지만, 만약 유효성 검증/네트워크 등 무거운 작업이 연결되어 있다면 didChange에서 계속 호출되는 구조가 부담이 될 수 있습니다.
  • 이전 PR의 RoutineCreationInputView에서, 텍스트 변경 타이밍에 따라 입력 중 텍스트가 손실되는 이슈를 피하기 위해 delegate에 “변경 전 텍스트”를 넘기던 패턴이 있었던 것으로 기억합니다. 여기서도 비슷한 delegate 흐름을 사용할 계획이라면, 입력 중 다른 액션(예: 다른 버튼 탭)으로 인해 텍스트가 유실되지 않는지 한 번만 확인 부탁드립니다.

현재 구현 자체는 문제 없어 보이지만, 의도한 UX/성능 요구사항과 맞는지 확인해 보시면 좋겠습니다.

Based on learnings

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

47-47: 날짜 변환 실패 시 빈 문자열 대신 대체 표시를 고려하세요.

날짜 파싱에 실패하면 빈 문자열이 표시되어 사용자에게 혼란을 줄 수 있습니다. 원본 날짜(reportEntity.date)를 표시하거나 "날짜 정보 없음"과 같은 플레이스홀더를 사용하는 것을 고려해보세요.

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

163-166: 에러 처리 구현 필요

TODO 주석에 명시된 대로 리포트 조회 실패 시 에러 처리가 필요합니다. 사용자에게 적절한 피드백을 제공하거나 로깅을 추가하는 것을 고려하세요.

에러 처리 구현을 도와드릴까요? 예를 들어 에러 publisher를 추가하거나 알림을 표시하는 코드를 생성해드릴 수 있습니다.

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

9-17: 프로퍼티 명명이 불명확합니다.

file1, file2, file3additionalProp1/2/3 같은 이름은 임시 또는 예제 API 응답처럼 보입니다. 실제 API 응답 구조를 반영하는 더 명확한 이름을 사용하는 것을 권장합니다.

실제 API 명세를 확인하여 의미 있는 프로퍼티명으로 변경하거나, 이것이 의도된 API 구조라면 주석으로 설명을 추가해주세요.

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

15-19: description을 API/표시 용도로 어디까지 쓸지 한 번만 정리해 두면 좋겠습니다

현재 descriptionrawValue.uppercased()를 반환해서 "TRANSPORTATION" 같은 코드값을 얻기에는 편리한데, 이 값이

  • 서버 파라미터용 코드인지
  • UI 표시용(혹은 디버그용) 문자열인지

역할이 섞일 수 있습니다.

API 파라미터/코드값으로 쓰실 계획이라면:

  • description보다는 var apiCode: String { rawValue.uppercased() } 같은 명시적인 프로퍼티를 하나 더 두고,
  • UI에는 별도의 name/displayName(이미 사용 중이라면 그대로 유지) 프로퍼티를 쓰는 쪽이 나중에 혼동을 줄여줄 것 같습니다.

현재 코드 자체는 동작상 문제는 없어 보이고, 향후 역할 분리가 필요할 때 고려해 볼 만한 정도입니다.

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

82-91: 등록 완료/실패 플로우 구성은 적절하고, 실패 UX와 스레드만 한 번 체크 부탁드립니다

  • 성공 시: ReportLoadingViewController에서 ReportCompleteViewController로 push 하는 구조가 현재 설계(등록 → 로딩 → 완료)에 잘 맞습니다.
  • 실패 시: 단순히 popViewController로 돌아가게 되어 있는데, 네트워크 오류 등에서는 한 줄 정도의 에러 토스트/알럿을 보여준 뒤 돌아가도록 확장해도 좋을 것 같습니다.
  • 스레드: 이 메서드 안에서 push/pop을 수행하므로, ReportRegistrationViewController 쪽에서 delegate를 반드시 메인 스레드에서 호출하고 있는지만 한 번만 확인해 주세요(receive(on: .main) 등을 이미 쓰고 계시면 그대로 괜찮습니다).

reportId 전달은 TODO에 적어두신 것처럼, 나중에 상세 조회 구현 시 ReportCompleteViewController 초기화 시점에서 넘길 수 있도록 이니셜라이저/프로퍼티만 정의해 주시면 될 것 같습니다.

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

270-283: 날짜별 그룹핑/정렬 로직은 괜찮고, 정렬 기준을 별도로 둘지 정도만 고민해 보면 좋겠습니다

  • Dictionary(grouping: reports) { $0.date }로 섹션 ID(날짜 문자열) 기준 그룹핑 후, 키를 내림차순 정렬해서 최신 날짜 섹션이 위로 오도록 한 부분은 의도와 잘 맞습니다.
  • animatingDifferences: false로 변경해서 히스토리 필터링 시 테이블 뷰 깜빡임이 줄어드는 것도 좋아 보입니다.

한 가지 옵션으로, ReportHistoryItem.date가 완전히 표시용 문자열(예: "11월 22일 토요일")이라면
향후 포맷 변경 시 문자열 비교(>)가 실제 시간 역순과 어긋날 수 있어서, 도메인 모델에 sortDate: Date 같은 정렬 전용 값을 따로 두고 그 기준으로 정렬한 뒤, 섹션 ID만 현재처럼 문자열로 사용하는 구조도 고려해 볼 만합니다.

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

11-12: 의존성 주입 고려를 권장합니다.

NetworkService.shared를 직접 사용하는 대신 생성자를 통해 주입받는 것을 고려하세요. 이렇게 하면 테스트 가능성이 향상되고, 의존성이 명시적으로 드러납니다.

 final class FileRepository: FileRepositoryProtocol {
-    private let networkService = NetworkService.shared
+    private let networkService: NetworkServiceProtocol
+    
+    init(networkService: NetworkServiceProtocol = NetworkService.shared) {
+        self.networkService = networkService
+    }
Projects/Domain/Sources/Protocol/Repository/ReportRepositoryProtocol.swift (1)

12-26: report 메서드 시그니처와 주석 파라미터 이름 통일 제안

함수 자체 시그니처와 역할은 ReportUseCase/Repository 구현과 잘 맞습니다만,
주석에서는 파라미터 이름이 photos로 되어 있고 실제 시그니처는 photoURLs라서 약간 혼동 소지가 있습니다. 주석의 파라미터 이름을 photoURLs로 맞추거나, 함수 파라미터를 photos로 변경해 통일하는 편이 가독성 측면에서 더 좋을 것 같습니다.

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

670-676: DI 해석 실패 fatalError 메시지 오타 정리 제안

guard let reportRegistrationViewModel = ...에서 실패 시 fatalError 메시지가 "reportRegistrationViewController 의존성이 등록되지 않았습니다."로 되어 있어, 실제로 누락된 것은 ViewModel인지 ViewController인지 한 번 더 보게 됩니다.

메시지를 "ReportRegistrationViewModel 의존성이 등록되지 않았습니다." 정도로 맞춰 두면 디버깅 시 더 명확할 것 같습니다. 그 외 DI 및 네비게이션 플로우는 기존 패턴과 일관적입니다.

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

9-39: ReportEntity 필드 확장 설계는 적절하나 photoUrls/URLs 네이밍 통일 권장

  • idInt?로 바꾸고 thumbnailURL, photoURLs를 추가한 구조 변경은 DTO(ReportDTO) 변경 사항 및 “상세 응답에 id가 없을 수 있음 + 썸네일/다중 이미지 지원” 요구사항과 잘 맞습니다.
  • 다만 이니셜라이저 마지막 파라미터 이름이 photoUrls이고, 프로퍼티는 photoURLs라서 타입 이름/프로퍼티 이름 일관성이 조금 떨어집니다.

이 부분만 photoURLs로 통일하거나, 기존 컨벤션에 맞게 둘 다 photoUrls로 맞추면 이후 호출부에서 혼동을 줄일 수 있을 것 같습니다.

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

15-62: S3Endpoint의 baseURL/path 구성 방식 NetworkService와 일관성 확인 필요

현재 구현은:

var baseURL: String { "" }

var path: String {
    switch self {
    case .uploadImage(let uploadURL, _):
        return uploadURL
    }
}

즉, presigned URL 전체를 path에 넣고 baseURL은 빈 문자열로 두는 패턴입니다.

NetworkService가 URL을 만들 때:

  • URL(string: baseURL + path) 와 같이 단순 문자열 합성을 한다면 문제 없지만,
  • URLComponents(string: baseURL)path를 설정하는 구조라면 baseURL이 빈 문자열일 때 URL 생성이 실패할 수 있습니다.

기존 다른 Endpoint와 동일한 방식으로 동작하는지 한 번만 확인 부탁드립니다. 필요하다면:

  • baseURL"https://..." 혹은 nil 허용 구조로 바꾸거나,
  • presigned URL 전용 코드 경로에서 path를 그대로 URL(string:)에 넘기도록

정리하는 것도 고려해 볼 수 있습니다.

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

47-48: 내용 글자 수 라벨 구현은 OK, 상수 공유 리팩터 제안

내용 글자 수 카운트 라벨을 위한 레이아웃/스타일/바인딩이 일관되게 잘 연결되어 있습니다(contentPublisher에서 즉시 "현재 글자 수 / 150" 포맷으로 갱신).

다만 "150" 상한 값이 여기 문자열로 박혀 있어, 뷰모델 검증이나 다른 곳에서도 같은 제한을 쓰고 있다면 상수를 한 곳에서 관리하는 편이 좋습니다(예: Layout 혹은 별도의 Constants.maxContentLength). 나중에 요구사항이 바뀌어 200자로 변경될 때 실수할 가능성을 줄일 수 있습니다.

Also applies to: 68-68, 127-129, 148-148, 284-294, 371-378


59-59: 섹션 레이아웃 재구성은 자연스러움, 위치 타이틀 기준 뷰만 한 번 점검 권장

사진 → 제목(nameTitleLabel + reportTitleTextView) → 카테고리 → 내용 → 위치 순으로 타이틀/입력 뷰를 재배치한 부분이 전체 플로우상 자연스럽고, 각 뷰의 top/leading/height 제약도 충돌 없이 잘 잡혀 있습니다.

한 가지만 디자인 관점에서 한 번 더 확인해볼 만한 부분은 다음입니다.

  • contentTextCountLabel의 top은 reportContentTextView.snp.bottom + 6
  • locationTitleLabel의 top은 여전히 reportContentTextView.snp.bottom + Layout.titleLabelTopSpacing(=28) 기준

결과적으로 내용 텍스트뷰 아래에
텍스트뷰 → 6pt → 카운트 라벨(18pt) → 4pt → 위치 타이틀
이 되는 구조인데, 만약 “내용 블록(텍스트뷰+카운트 라벨) 전체 기준으로 위치 타이틀까지 28pt 간격”을 의도한 디자인이라면, locationTitleLabel의 top anchor를 contentTextCountLabel.snp.bottom으로 옮기는 것도 고려해볼 수 있습니다. 지금 구조도 레이아웃 상 문제는 없고, 완전히 취향/디자인 이슈라 선택 사항입니다.

Also applies to: 61-61, 66-66, 140-140, 205-242, 257-268, 270-282, 296-307

📜 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 4239784.

⛔ 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/FilePresignedConditaionDTO.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/FIileRepositoryProtocol.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 (1 hunks)
  • Projects/Presentation/Sources/Report/View/ReportHistoryViewController.swift (2 hunks)
  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1 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)
🧰 Additional context used
🧠 Learnings (4)
📚 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/ReportRegistrationViewController.swift
  • Projects/Presentation/Sources/Home/View/HomeViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportCompleteViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.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/Home/View/HomeViewController.swift
  • Projects/Presentation/Sources/Report/View/ReportLoadingViewController.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-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
🧬 Code graph analysis (17)
Projects/Domain/Sources/DomainDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
Projects/DataSource/Sources/Repository/ReportRepository.swift (3)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-76)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (1)
  • register (134-172)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
Projects/DataSource/Sources/Endpoint/FilePresignedEndpoint.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
  • fetchPresignedURL (14-19)
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-76)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (2)
  • fetchCurrentLocation (25-29)
  • report (39-76)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Presentation/Sources/Report/View/ReportRegistrationViewController.swift (2)
Projects/Presentation/Sources/Report/View/ReportLoadingViewController.swift (1)
  • reportRegistrationViewController (83-91)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (2)
  • action (64-81)
  • register (134-172)
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 (83-91)
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
Projects/Domain/Sources/Protocol/UseCase/ReportUseCaseProtocol.swift (2)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (4)
  • fetchCurrentLocation (25-29)
  • fetchReports (31-33)
  • fetchReport (35-37)
  • report (39-76)
Projects/DataSource/Sources/Repository/ReportRepository.swift (2)
  • fetchReports (41-53)
  • report (14-39)
Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift (3)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (1)
  • register (134-172)
Projects/DataSource/Sources/Repository/ReportRepository.swift (1)
  • report (14-39)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift (1)
  • report (39-76)
Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift (1)
Projects/DataSource/Sources/Repository/FileRepository.swift (2)
  • fetchPresignedURL (14-19)
  • uploadFile (21-24)
Projects/DataSource/Sources/Repository/FileRepository.swift (1)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
  • request (25-48)
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/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift (1)
  • register (134-172)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
Projects/DataSource/Sources/NetworkService/Extension/URLRequest+.swift (1)
  • makeBodyParameter (33-36)
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)
🪛 SwiftLint (0.57.0)
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

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

(todo)

⏰ 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 (43)
Projects/Shared/Sources/Extension/Date+.swift (1)

45-58: 새로 추가된 포맷 매핑("yyyy-MM-dd E", "yyyy-MM-dd (E)")은 기존 패턴과 잘 정합됩니다.

convertToString / convertToDate 양쪽 모두 동일한 formatString을 사용하므로, 역직렬화 측면에서도 일관성이 유지됩니다. 현재 형태 그대로 사용해도 무방해 보입니다.

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

23-31: 체브론 사이즈 상수 변경은 문제 없어 보입니다

chevronImageWidth / chevronImageHeight를 디자인에 맞게 조정한 것으로 보이고, 제약 조건도 width/height만 사용하는 단순 케이스라 레이아웃 상 이슈는 없어 보입니다.

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

53-53: LGTM!

photoUrls 파라미터명 변경이 엔티티 변경사항과 일치합니다.


43-44: 날짜 변환 로직의 견고성을 확인하세요.

문자열의 형식이 DateFormatter의 날짜 형식과 일치하지 않으면 date(from:)는 nil을 반환합니다. reportEntity.date가 nil이거나 .yearMonthDate 형식과 일치하지 않을 경우, 변환이 실패하여 최종적으로 빈 문자열이 표시됩니다.

다음 사항을 확인하는 것을 권장합니다:

  1. reportEntity.date가 항상 .yearMonthDate 형식과 일치하는지 검증
  2. 변환 실패 시 원본 날짜 또는 의미 있는 플레이스홀더(예: "날짜 없음") 표시를 고려
Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift (5)

34-34: 내부 상태 관리 개선

공개 publisher를 제거하고 private 상태 변수로 교체한 것은 적절한 캡슐화입니다.


88-96: 진행 상태 선택 로직 정상

선택된 진행 상태만 isSelected = true로 설정하고 나머지는 false로 처리하는 로직이 올바릅니다.


106-111: 필터링 로직 정확함

.entire 케이스를 제외하고 선택된 진행 상태로 필터링하는 로직이 정확합니다.


131-139: 엔티티 매핑 정상

id guard 후 사용과 thumbnailURL 필드명 변경이 올바르게 적용되었습니다. 다만 thumbnailUrldate에 빈 문자열 fallback이 사용되는데, UI에서 빈 값 처리가 적절한지 확인하세요.


119-167: 메인 스레드 디스패치 확인 필요

Task 블록 내에서 progressSubjectreportSubject에 값을 전송하고 있습니다. 이러한 publisher들이 UI 업데이트에 사용되는 경우, 메인 스레드에서 실행되는지 확인이 필요합니다.

Combine의 기본 동작 또는 구독 측에서 메인 스레드 디스패치를 처리하는지 확인하세요. 필요하다면 다음과 같이 명시적으로 메인 스레드로 전환하는 것을 고려하세요:

await MainActor.run {
    progressSubject.send(progressItems)
    reportSubject.send(reportHistoryItems)
}
Projects/DataSource/Sources/Common/Enum/EndpointBodyType.swift (1)

8-11: LGTM!

엔드포인트 바디 타입을 구분하는 간결하고 명확한 enum입니다. JSON과 raw data 처리를 위한 타입 안전성을 제공합니다.

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

29-31: 제보 유효성 검증 로직이 잘 구현되었습니다.

사용자 입력이 변경될 때마다 verifyIsReportValid()를 호출하여 실시간으로 제보 유효성을 검증하는 reactive 패턴이 적절하게 적용되었습니다.

Also applies to: 42-43, 58-60, 86-87, 91-92, 96-97, 104-105, 107-108, 125-126, 131-132


147-168: 최소 로딩 시간 패턴을 확인해주세요.

0.7초의 최소 지속시간은 로딩 인디케이터 UX를 위한 의도적인 선택으로 보입니다. 이것이 제품 요구사항인지 확인하고, 그렇다면 주석으로 의도를 명시하는 것을 권장합니다.

+        // UX: 로딩 인디케이터가 너무 빨리 사라지지 않도록 최소 0.7초 유지
         let minimumDuration: TimeInterval = 0.7

174-190: LGTM!

모든 필수 필드(제목, 카테고리, 내용, 위치 좌표, 사진)를 체크하는 포괄적인 검증 로직입니다.

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

8-8: 아이콘 프로퍼티가 올바르게 구현되었습니다.

SelectableItem 프로토콜을 확장하여 카메라와 라이브러리 아이콘을 적절히 매핑했습니다. PR 설명에 언급된 것처럼 아이콘이 현재 표시되지 않는 것은 알려진 문제입니다.

Also applies to: 38-45

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

1-23: LGTM!

표준 Xcode 애셋 카탈로그 형식입니다.

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

15-22: LGTM!

bodyType에 따라 JSON 또는 raw data 바디를 적절히 처리하는 명확한 분기 로직입니다. S3 업로드와 같은 raw data 전송을 지원하면서도 기존 JSON 엔드포인트와의 호환성을 유지합니다.

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

1-23: LGTM!

표준 Xcode 애셋 카탈로그 형식입니다.

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

81-83: 빈 응답 처리 구현이 올바르게 검증되었습니다.

EmptyResponseDTOProjects/DataSource/Sources/DTO/EmptyResponseDTO.swift에서 Decodable을 준수하는 구조체로 정확히 정의되어 있으며, RoutineRepository, FileRepository, AuthRepository, OnboardingRepository 등 8개 위치에서 일관되게 사용되고 있습니다. NetworkService의 타입 체크 패턴(T.self == EmptyResponseDTO.self)은 타입 안전하고 명시적으로 구현되어 있어 데이터가 없는 응답을 처리하는 데 적절합니다.

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

106-111: 확인 버튼에서 루트로 돌아가는 내비게이션 플로우 적절합니다

[weak self] 캡처로 메모리 이슈도 없고, 완료 화면에서 홈(루트)으로 바로 돌아가는 UX도 의도와 잘 맞습니다. 현재 구조(항상 내비게이션 스택 상에서 push되는 화면)라면 이 구현 그대로 사용해도 될 것 같습니다.

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

52-54: FileRepository DI 등록 구성 괜찮습니다

다른 레포지토리들과 동일한 패턴으로 FileRepositoryProtocolFileRepository를 등록해서, DomainDependencyAssemblerReportUseCase 구성 시 fileRepository를 resolve 하는 흐름과 잘 맞습니다.

preAssembler.assemble()가 항상 DataSource 쪽을 먼저 호출한다는 전제만 유지되면 DI 관점에서 문제 없어 보입니다.

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

69-76: ReportUseCase에 FileRepository DI 추가 구성 잘 되어 있습니다

ReportUseCaseProtocol 등록 시

  • LocationRepositoryProtocol
  • ReportRepositoryProtocol
  • FileRepositoryProtocol

세 가지를 모두 resolve하고, 이를 ReportUseCase 이니셜라이저에 주입하는 구조가 전체 DI 흐름(DataSource에서 Repository 구현 등록 → Domain에서 UseCase 구성)과 잘 맞습니다.

기존 다른 UseCase들과 동일하게, 필수 의존성이 누락되면 fatalError로 바로 드러나도록 한 것도 현재 프로젝트 스타일과 일관적이라 괜찮아 보입니다.

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

10-23: LGTM!

프로토콜 설계가 명확하고 문서화가 잘 되어 있습니다. presigned URL 발급과 파일 업로드를 위한 async/throws 메서드가 적절하게 정의되어 있습니다.

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

18-20: LGTM!

새로운 프로토콜 요구사항이 추가되었지만, 기본 구현을 제공하여 하위 호환성을 유지하고 있습니다. JSON 바디와 raw 데이터 바디를 모두 지원할 수 있는 유연한 설계입니다.


22-25: LGTM!

기본 구현이 적절합니다. 기존 엔드포인트들은 JSON 바디를 사용하고, raw 데이터가 필요한 엔드포인트만 이 속성들을 오버라이드하면 됩니다.

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

14-17: LGTM!

레이아웃 상수가 적절하게 정의되어 있습니다. 아이콘 크기(20)가 스택 뷰 높이(24) 내에 잘 맞습니다.


20-21: LGTM!

UI 컴포넌트 초기화와 설정이 올바릅니다. iconImageView를 처음에 숨기고 아이콘이 있을 때만 표시하는 것은 좋은 접근입니다.

Also applies to: 36-38, 48-48


83-86: 코드 검증 완료 - 리뷰 의견은 수정이 필요합니다.

SelectPhotoType 및 이미지 에셋에 대한 검증 결과, 다음을 확인했습니다:

  • SelectPhotoType.icon (SelectPhotoType.swift:38-45): 올바르게 구현됨

    • .cameraBitnagilIcon.bitnagilCameraIcon 반환
    • .libraryBitnagilIcon.bitnagilPhotoIcon 반환
  • BitnagilIcon 정의 (BitnagilIcon.swift:61-62): 올바르게 정의됨

    • 두 아이콘 모두 올바른 에셋 이름으로 로드됨
  • 이미지 에셋: 존재하며 올바르게 구성됨

    • bitnagil_camera_icon.imageset, bitnagil_photo_icon.imageset 모두 1x, 2x, 3x 스케일 포함
  • SelectableItemCell 로직 (lines 83-86): 올바르게 구현됨

원래 리뷰 의견의 조사 제안들은 불필요합니다. 아이콘 비표시 문제가 실제로 존재한다면 이 코드 영역이 아닌 다른 곳(번들 멤버십, 런타임 설정 등)에서 원인을 찾아야 합니다.

Likely an incorrect or invalid review comment.

Projects/DataSource/Sources/Endpoint/ReportEndpoint.swift (4)

9-9: LGTM!

새로운 register 케이스가 추가되어 제보 등록 기능을 지원합니다.


15-17: LGTM!

baseURL 계산이 단순화되어 모든 케이스에 동일한 base URL을 사용합니다. 이전에 케이스별로 분기했던 것보다 깔끔합니다.


30-34: LGTM!

HTTP 메서드 매핑이 올바릅니다. register는 POST를 사용하고, 조회 작업들은 GET을 사용합니다.


49-56: 검증 완료 - 코드 변경 승인

ReportDTO.dictionary 속성이 정상적으로 구현되어 있습니다. ReportDTOCodable을 준수하며, Projects/DataSource/Sources/Common/Extension/Encodable+.swift에서 Encodable의 확장으로 dictionary 계산 프로퍼티를 제공합니다. 이 구현은 JSONEncoder를 통해 객체를 인코딩하고 다시 딕셔너리로 변환하는 표준적인 패턴이며, 에러 발생 시 빈 딕셔너리를 반환하여 안전하게 처리됩니다. ReportEndpoint의 코드는 정상적으로 작동합니다.

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

8-9: LGTM!

SelectableItem 프로토콜에 선택적 icon 속성이 추가되었습니다. 기본 구현이 nil을 반환하므로 기존 구현체들의 호환성이 유지됩니다. UIKit import도 적절하게 추가되었습니다.

Also applies to: 14-19

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

8-9: LGTM!

Data 타입 사용을 위해 Foundation import가 추가되었습니다.


15-22: LGTM!

제보 목록 조회와 단건 상세 조회 메서드가 명확하게 정의되었습니다. 문서화도 잘 되어 있습니다.


24-38: LGTM!

새로운 report 메서드 시그니처가 실제 사용 사례를 잘 반영하고 있습니다:

  • 사진을 [Data] 배열로 받아 업로드 처리
  • 등록된 제보의 id를 반환
  • 모든 파라미터가 명확하게 문서화됨

이는 이전 방식보다 더 실용적인 API입니다.

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

14-19: LGTM!

presigned URL 발급 구현이 명확합니다. 파일명 배열을 DTO로 변환하고 엔드포인트를 통해 요청하는 로직이 올바릅니다.


21-24: LGTM!

파일 업로드 구현이 적절합니다. S3 업로드는 일반적으로 빈 응답을 반환하므로 EmptyResponseDTO를 사용하고 결과를 무시하는 것이 맞습니다. 에러는 throws를 통해 전파됩니다.

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

79-80: 코드 변경사항 승인

이미지 에셋 검증 완료. bitnagil_camera_iconbitnagil_photo_icon 이미지셋이 정확하게 번들에 존재하며, 스케일 변형(@2x, @3x)도 모두 포함되어 있습니다. 코드의 구현이 올바르고, Report 섹션의 배치도 적절합니다.

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

14-38: ReportDTO 구성 및 등록 API 호출 로직 적절

입력 파라미터를 ReportDTO로 매핑하고 ReportEndpoint.register를 통해 Int id를 받아오는 흐름이 Domain ReportUseCase.report(...)와 잘 정렬되어 있습니다.

  • location이 없을 때 address를 빈 문자열로 내려주는 처리,
  • reportImageUrlsphotoURLs를 그대로 전달하는 부분,
  • 응답 id를 Int?로 감싸서 반환하는 부분

모두 현재 설계 의도와 맞아 보입니다.

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

10-54: ReportDTO의 Codable 전환 및 ReportEntity 매핑 로직 적절

  • ReportDTOCodable로 바꾼 덕분에 등록 요청 바디로도 재활용할 수 있고,
  • reportStatus를 Optional로 두고 ReportProgress(rawValue: reportStatus ?? "") ?? .received로 기본값을 .received로 두는 전략도 안전합니다.
  • 상세 응답에서 reportId가 없을 수 있는 케이스를 고려해 toReportEntity()에서는 guard를 제거하고, 히스토리 목록 등 id가 반드시 있어야 하는 경우에만 toReportEntity(date:)에서 guard let reportId를 유지한 것도 합리적인 분리로 보입니다.
  • thumbnailURLreportImageUrlsReportEntity.thumbnailURL / photoURLs 매핑도 일관적입니다.

현재 형태로 충분히 도메인 모델과 DTO가 잘 동기화되어 있는 것 같습니다.

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

15-67: 리뷰 의견은 부정확합니다. 코드는 올바르게 작성되어 있습니다.

  1. method 프로퍼티 컴파일 오류 없음
    Swift 5.9는 switch 표현식(SE-0380)을 지원하므로 case .fetchPresignedURL: .post 구문은 완벽하게 유효합니다. return 키워드를 생략해도 문제없습니다.

  2. baseURLpath 패턴은 정상
    ReportEndpoint, OnboardingEndpoint 등 다른 엔드포인트들도 동일한 패턴을 사용합니다:

    • baseURL = 완전한 베이스 경로 반환
    • path = baseURL + 추가경로 반환

    네트워크 레이어(URLRequest+.swift, Endpoint+.swift)는 path 값만 사용하여 URLRequest를 생성하므로 이 구조는 올바릅니다.

  3. bodyData 인코딩 에러 처리
    try? JSONEncoder().encode() 패턴은 Encodable+.swift 확장에서도 사용되는 코드베이스의 기존 관행입니다. 일관성 있는 설계 선택입니다.

Likely an incorrect or invalid review comment.

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

14-16: 등록 완료 델리게이트 흐름 구성 적절함

ReportRegistrationViewControllerDelegate로 등록 완료 이벤트를 분리하고, 버튼 탭 시 ReportLoadingViewController를 생성해서 delegate로 주입한 뒤, reportRegistrationCompletePublisher에서 main 스레드로 받아 delegate 메서드만 호출하는 구조가 깔끔합니다.
delegateweak으로 둔 것도 네비게이션 스택 기반 구조와 잘 맞습니다. 현재 플로우(버튼 탭 → 로딩 화면 push → 비동기 register → delegate 콜백) 기준으로는 별다른 이슈 없어 보입니다.

Also applies to: 74-74, 120-123, 418-425


407-416: isReportValid로 제출 버튼 상태 제어하는 바인딩 적절

viewModel.output.isReportValid를 main 스레드에서 구독하여 registerButton.updateButtonState.default / .disabled를 토글하는 방식이 명확하고, 기존 PrimaryButton 상태 모델과도 잘 맞습니다.

추가 로직 없이 검증 결과만으로 UI 상태를 직접 연결하고 있어 유지보수 측면에서도 괜찮은 구현입니다.

Comment on lines 1 to 7
//
// FIileRepository.swift
// Domain
//
// Created by 이동현 on 11/22/25.
//

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

파일명 오타를 수정하세요.

파일명에 오타가 있습니다: FIileRepository.swiftFileRepository.swift가 되어야 합니다. 대문자 I 대신 소문자 i를 사용해야 합니다.

-//  FIileRepository.swift
+//  FileRepository.swift
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//
// FIileRepository.swift
// Domain
//
// Created by 이동현 on 11/22/25.
//
//
// FileRepository.swift
// Domain
//
// Created by 이동현 on 11/22/25.
//
🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/Repository/FIileRepositoryProtocol.swift
around lines 1 to 7, the filename contains a typo ("FIileRepository.swift") and
should be renamed to "FileRepository.swift"; rename the file to correct the
capitalization and spelling, and update any references/usages (imports,
file-based targets, and any code that references the old filename) to the new
filename to avoid build or import errors.

Comment on lines +31 to +37
public func fetchReports() async throws -> [ReportEntity] {
return try await reportRepository.fetchReports()
}

public func fetchReport(reportId: Int) async throws -> ReportEntity? {
return try await reportRepository.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

🧩 Analysis chain

ReportUseCase.report 로직의 빈 배열 처리·URL 매핑·업로드 에러 처리 개선 필요

전체적인 플로우(사전 presigned URL 조회 → S3 업로드 → 공개 URL로 report 등록)는 잘 설계되어 있습니다만, 아래 몇 가지는 보완이 필요해 보입니다.

  1. photos가 비어 있을 때 Range 크래시 가능성

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

    photos.count == 0이면 1...0 Range 생성 시 런타임 크래시가 발생합니다. 현재 ViewModel에서 selectedPhotoSubject.value.count > 0을 보장하더라도, UseCase는 public API라 재사용 시 위험합니다.

    예시 수정안:

    public func report(..., photos: [Data]) async throws -> Int? {
  •  guard !photos.isEmpty else { return nil }
    
  •  let fileNames = (1...photos.count).map { "\($0).jpg" }
    
  •  let fileNames = (1...photos.count).map { "\($0).jpg" }
     ...
    
    }
    (혹은 `0..<photos.count`를 사용하는 방식 등으로 안전하게 처리)
    
    
    
  1. presigned URL과 사진의 매핑이 Dictionary 순서에 의존

    let presignedDict = try await fileRepository.fetchPresignedURL(...)
    let presignedURLs = Array(presignedDict.values)
    
    for (url, photo) in zip(presignedURLs, photos) { ... }

    Dictionaryvalues 순서는 정의되어 있지 않기 때문에, photos 배열 순서와 presigned URL이 1:1로 맞게 매핑된다고 보장할 수 없습니다. 서버가 fileName을 key로 응답해 준다면, fileNames를 key로 사용해 명시적으로 매핑하는 편이 안전합니다.

    예시 수정안:

  • let presignedURLs = Array(presignedDict.values)
  • let presignedURLs = fileNames.compactMap { presignedDict[$0] }
  • guard presignedURLs.count == photos.count else { return nil }
    이렇게 하면 presigned URL 순서가 항상 원래 선택한 사진 순서와 일치합니다.
    
    
    
  1. 사진 업로드 실패 시 조용히 무시하는 TODO

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

    현재는 업로드 실패를 로그만 찍고 무시한 뒤, 여전히 publicImageURLs 전체를 report 등록에 사용합니다. 일부 이미지만 빠지거나 깨진 상태로 등록될 수 있고, // TODO: - 사진 업로드 실패 시 에러 처리 필요와 SwiftLint 경고도 그대로입니다.

    최소한:

    • 업로드 실패 시 throw로 전체 등록을 중단하거나,
    • 실패한 URL을 제외하고 publicImageURLs를 구성하되, 사용자에게 “일부 사진 업로드 실패”를 알려주는 방식

    중 하나로 처리하는 것을 권장합니다.

위 세 부분만 정리되면, 새로운 파일 업로드 + 제보 등록 유즈케이스는 꽤 견고해질 것 같습니다.

Also applies to: 39-75


🏁 Script executed:

#!/bin/bash
# Retrieve the full ReportUseCase.swift file to verify the code issues
find . -name "ReportUseCase.swift" -type f | head -1 | xargs cat

Length of output: 2416


🏁 Script executed:

#!/bin/bash
# Check if there are any guard statements or validation for photos array
rg -n "photos" Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift -A 2 -B 2

Length of output: 684


🏁 Script executed:

#!/bin/bash
# Check the fileRepository interface to understand presigned URL response structure
find . -name "*FileRepository*" -type f | grep -E "\.(swift|kt)$" | head -5

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Look for uploadFile method signature
rg -n "uploadFile" --type swift -A 3 -B 1

Length of output: 1362


🏁 Script executed:

#!/bin/bash
# Verify Swift Range behavior with zero-count - check if there are any comments or test cases
rg -n "1\.\.\..*\.count" --type swift -C 2

Length of output: 465


🏁 Script executed:

#!/bin/bash
# Check ViewModel to see if there's any validation before calling UseCase
rg -n "selectedPhotoSubject" --type swift -C 3 | head -30

Length of output: 3842


🏁 Script executed:

#!/bin/bash
# Look for Dictionary.values usage patterns in Swift codebase
rg -n "Dictionary.*values" --type swift -C 2 | head -40

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify if there are TODO comments about error handling
rg -n "TODO.*사진|TODO.*upload|TODO.*error" Projects/Domain/Sources/UseCase/Report/ -C 1

Length of output: 240


🏁 Script executed:

#!/bin/bash
# Check the actual presigned URL response structure
rg -n "fetchPresignedURL" --type swift -A 5 -B 2

Length of output: 6820


🏁 Script executed:

#!/bin/bash
# Look for the report method implementation
ast-grep --pattern 'public func report($$$) async throws -> $_'

Length of output: 3559


🏁 Script executed:

#!/bin/bash
# Check Swift version to confirm Range crash behavior
find . -name ".swift-version" -o -name "Package.swift" -o -name "project.pbxproj" | head -3 | xargs cat 2>/dev/null | head -20

Length of output: 522


🏁 Script executed:

#!/bin/bash
# Verify if there's a check for empty photos before using Range
rg -n "photos\.isEmpty|photos\.count.*==.*0|guard.*photos" Projects/Domain/Sources/UseCase/Report/ -B 2 -A 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check imports to understand fileRepository type
rg -n "fileRepository|FileRepository" Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift -B 5 | head -20

Length of output: 732


🏁 Script executed:

#!/bin/bash
# Look at the actual zip operation to understand the mapping issue better
rg -n "zip.*presigned" --type swift -C 3

Length of output: 662


🏁 Script executed:

#!/bin/bash
# Check for SwiftLint disable comments
rg -n "swiftlint" Projects/Domain/Sources/UseCase/Report/ -i

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify the complete report method signature and body
sed -n '39,75p' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

Length of output: 1197


🏁 Script executed:

#!/bin/bash
# Look for any presigned URL struct/model definition
rg -n "PresignedURL|PresignedURLResponse" --type swift -B 2 -A 3 | head -40

Length of output: 3817


🏁 Script executed:

#!/bin/bash
# Find the type of presignedDict to understand Dictionary behavior
ast-grep --pattern 'let presignedDict = $_'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any unit tests for the report method
find . -name "*ReportUseCase*Test*" -o -name "*Test*ReportUseCase*" | xargs ls -la 2>/dev/null

Length of output: 905


🏁 Script executed:

#!/bin/bash
# Verify the complete context around lines 31-75
sed -n '1,80p' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

Length of output: 2416


🏁 Script executed:

#!/bin/bash
# Check if there's error handling wrapper that might mitigate the issues
rg -n "try await reportRepository\.report|\.report.*throws" --type swift -C 2

Length of output: 426


🏁 Script executed:

#!/bin/bash
# Look for publicImageURLs usage
rg -n "publicImageURLs" Projects/Domain/Sources/UseCase/Report/ -B 2 -A 2

Length of output: 888


🏁 Script executed:

#!/bin/bash
# Verify how the API client sends the publicImageURLs
rg -n "reportRepository.*report" --type swift -A 3 | head -30

Length of output: 2415


🏁 Script executed:

#!/bin/bash
# Check if there are any validation guards in the report method
sed -n '39,75p' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift | rg "guard|if let"

Length of output: 13


🏁 Script executed:

#!/bin/bash
# Look for the actual value count comparison for presignedURLs
rg -n "presignedURLs.*count|count.*presigned" --type swift -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Examine the entire ReportUseCase class structure
rg -n "^class\|^public class\|^struct\|^public struct" Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the fileRepository protocol/interface definition
rg -n "protocol FileRepository|class FileRepository" --type swift -A 15 | head -40

Length of output: 3271


🏁 Script executed:

#!/bin/bash
# Look for the actual implementation of uploadFile behavior
rg -n "func uploadFile" --type swift -A 10 | head -30

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Verify error handling in upload loop
sed -n '50,70p' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift 2>/dev/null || echo "File not found or lines out of range"

Length of output: 729


🏁 Script executed:

#!/bin/bash
# List all files in the ReportUseCase directory to understand structure
ls -la Projects/Domain/Sources/UseCase/Report/ 2>/dev/null || find . -path "*/UseCase/Report/*" -type f

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Get line count of the file to confirm it has lines 39-75
wc -l Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Examine lines around the issue areas
sed -n '31,80p' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift 2>/dev/null

Length of output: 1495


🏁 Script executed:

#!/bin/bash
# Search for the report method in the entire domain folder
rg -n "public func report\(" Projects/Domain/Sources/ --type swift -A 5

Length of output: 532


🏁 Script executed:

#!/bin/bash
# Check if there's any Swift version constraint
cat package.resolved 2>/dev/null | grep -i "swift\|macos" | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify the presignedDict keys match fileNames
rg -n "fileNames\|presignedDict" Projects/Domain/Sources/UseCase/Report/ -C 1

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the actual report method definition with full signature
rg -n "func report.*photos.*async" --type swift -A 40 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the repository structure
find Projects/Domain/Sources -type d | head -20

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Get exact line numbers and content
awk 'NR>=31 && NR<=75' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift 2>/dev/null || awk 'NR>=1 && NR<=100' Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift 2>/dev/null | tail -80

Length of output: 1487


ReportUseCase.report 메서드의 빈 배열 처리·URL 매핑·업로드 에러 처리 개선 필수

전체 플로우(사전 presigned URL 조회 → S3 업로드 → 공개 URL로 report 등록)는 잘 설계되었으나, 아래 세 가지 문제가 확인되었습니다.

  1. photos가 비어 있을 때 Range 크래시 위험
    라인 46: let fileNames = (1...photos.count).map { "\($0).jpg" }
    photos.count == 0이면 1...0 Range 생성 시 런타임 크래시가 발생합니다. ViewModel의 검증(selectedPhotoSubject.count > 0)이 있더라도, UseCase는 public API로 재사용 될 수 있으므로 이 메서드 내에서 방어적으로 처리해야 합니다. guard !photos.isEmpty else { return nil } 또는 guard let first = photos.first else { return nil } 로 먼저 검증하세요.

  2. presigned URL과 사진의 매핑이 Dictionary 순서에 의존
    라인 54-56:

    let presignedURLs = Array(presignedDict.values)
    for (url, photo) in zip(presignedURLs, photos) { ... }

    서버가 반환하는 presignedDict[String: String] 형태로 key가 fileName, value가 URL입니다(protocol 주석 참고). Dictionary의 values 순서는 보장되지 않으므로, 원래 fileNames 순서와 일치한다고 보장할 수 없습니다. 대신 fileNames를 key로 명시적으로 매핑하세요:

    let presignedURLs = fileNames.compactMap { presignedDict[$0] }
    guard presignedURLs.count == fileNames.count else { return nil }
  3. 사진 업로드 실패 시 조용히 무시하는 문제
    라인 48의 TODO 주석과 라인 57-61의 try-catch 구문에서 업로드 실패를 print로만 로깅하고 계속 진행합니다. 결과적으로 일부 이미지만 성공한 상태에서도 모든 presignedURLs를 report 등록에 사용하게 되어, 깨진 이미지 링크가 저장될 수 있습니다. 업로드 실패 시 throw하거나, 실패한 URL을 제외하고 사용자에게 알리는 방식으로 개선하세요.

🤖 Prompt for AI Agents
Projects/Domain/Sources/UseCase/Report/ReportUseCase.swift around lines 31-61:
the report upload flow must defensively handle empty photos, reliably map
presigned URLs to filenames, and surface upload failures; first add a guard to
return early (or throw) when photos.isEmpty to avoid 1...0 range crashes; build
fileNames then derive presigned URLs by looking up each filename in the returned
presignedDict (e.g., compactMap over fileNames and guard the counts match)
instead of relying on Dictionary.values order; during the upload loop, treat
upload failures as errors (throw) or collect failed uploads and return an
error/result indicating which uploads failed rather than only printing and
continuing so only successfully uploaded URLs are registered with the report.

Comment on lines +126 to +129
let date = Date.convertToDate(from: reportEntity.date ?? "", dateType: .yearMonthDate)
let dateString = date?.convertToString(dateType: .yearMonthDateWeek)

guard let id = reportEntity.id else { continue }
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

🧩 Analysis chain

날짜 형식 검증 및 에러 처리 필요

날짜 변환 로직에서 다음 사항들을 확인해야 합니다:

  • reportEntity.datenil이거나 잘못된 형식일 경우 dateString은 빈 문자열이 됩니다.
  • Line 129에서 id가 없는 리포트는 조용히 건너뛰어집니다.

다음을 고려하세요:

  • 날짜 변환 실패 시 로깅 추가
  • 건너뛴 리포트 수를 추적하거나 로그에 기록

🏁 Script executed:

#!/bin/bash
# Description: API 응답에서 date 필드의 형식이 DateType.yearMonthDate와 일치하는지 확인

# ReportDTO의 date 필드 타입과 사용처 검색
ast-grep --pattern 'struct ReportDTO {
  $$$
  date: $TYPE
  $$$
}'

# DateType.yearMonthDate 형식 확인
rg -n "case yearMonthDate" -A 2 -B 2

Length of output: 587


🏁 Script executed:

#!/bin/bash
# Find ReportDTO structure definition
fd -e swift | xargs grep -l "struct ReportDTO\|class ReportDTO" | head -5

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Find the actual format strings for DateType cases
rg -n "case yearMonthDate|case yearMonthDateWeek" -A 5 --type=swift | head -40

Length of output: 668


🏁 Script executed:

#!/bin/bash
# Find convertToDate and convertToString method implementations
rg -n "func convertToDate|func convertToString" -A 10 --type=swift | head -60

Length of output: 1569


🏁 Script executed:

#!/bin/bash
# Get the formatString computed property for DateType
rg -n "var formatString|let formatString" -A 30 --type=swift Projects/Shared/Sources/Extension/Date+.swift

Length of output: 599


🏁 Script executed:

#!/bin/bash
# Read ReportDTO to understand the date field type
cat Projects/DataSource/Sources/DTO/ReportDTO.swift

Length of output: 1736


🏁 Script executed:

#!/bin/bash
# Get more context around lines 126-129 in ReportHistoryViewModel
rg -n "convertToDate" -B 10 -A 10 --type=swift Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift

Length of output: 979


날짜 변환 실패 시 데이터 손실 – 로깅 및 에러 처리 추가 필요

검증 결과, 라인 126-135의 코드는 다음과 같이 동작합니다:

  • reportEntity.date가 nil이면 빈 문자열 ""을 전달하고, convertToDate(from: "", dateType: .yearMonthDate)는 형식 "yyyy-MM-dd"와 일치하지 않으므로 nil을 반환합니다.
  • dateString은 nil이 되고, 라인 135의 dateString ?? ""로 인해 결과적으로 빈 문자열로 변환됩니다.
  • 중요한 점: 날짜가 유효하지 않은 리포트는 건너뛰지 않고 빈 날짜 문자열로 추가됩니다.

이는 사용자에게 표시되는 리포트 목록에서 날짜 정보가 손실되는 문제를 야기합니다. 다음을 권장합니다:

  1. 날짜 파싱 실패 시 로깅 추가
  2. 유효하지 않은 리포트를 건너뛰거나 기본값 처리 검토
  3. 유효하지 않은 날짜로 인해 건너뛴/수정된 리포트 수를 추적
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Report/ViewModel/ReportHistoryViewModel.swift
around lines 126–129, the current code converts reportEntity.date (or "" if nil)
to a Date and then to a string, which yields an empty date string on parse
failure; change this to (1) attempt to parse only when reportEntity.date is
non-nil/non-empty, (2) if parsing fails, log the failure with report id/context
and skip the invalid report (continue) or apply a clear default policy, and (3)
increment/record a counter for skipped/modified reports and emit a summary log
after processing; implement the guard so invalid-date cases do not silently
produce empty strings displayed to users.

Comment on lines 122 to +172
private func register() {
guard
let name = titleSubject.value,
!name.isEmpty,
let category = categorySubject.value,
let content = contentSubject.value,
let location,
selectedPhotoSubject.value.count > 0
else { return }

let selectedPhotos = selectedPhotoSubject.value.map({ $0.data })

Task {
let minimumDuration: TimeInterval = 0.7
let startTime = Date()
let reportId: Int?

do {
reportId = try await reportUseCase.report(
title: name,
content: content,
category: category,
location: location,
photos: selectedPhotos)
} catch {
reportId = nil
}

let elapsed = Date().timeIntervalSince(startTime)
if elapsed < minimumDuration {
let remaining = minimumDuration - elapsed
try? await Task.sleep(
nanoseconds: UInt64(remaining * 1_000_000_000)
)
}

reportRegistrationCompleteSubject.send(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

에러 발생 시 사용자에게 알림이 누락되었습니다.

제보 등록 실패 시 에러를 catch하지만 exceptionSubject로 사용자에게 알리지 않습니다. 사용자는 등록이 실패한 이유를 알 수 없습니다.

다음과 같이 에러 처리를 개선하세요:

         Task {
             let minimumDuration: TimeInterval = 0.7
             let startTime = Date()
             let reportId: Int?

             do {
                 reportId = try await reportUseCase.report(
                     title: name,
                     content: content,
                     category: category,
                     location: location,
                     photos: selectedPhotos)
             } catch {
+                exceptionSubject.send("제보 등록에 실패했습니다. 다시 시도해주세요.")
                 reportId = nil
             }

             let elapsed = Date().timeIntervalSince(startTime)
             if elapsed < minimumDuration {
                 let remaining = minimumDuration - elapsed
                 try? await Task.sleep(
                     nanoseconds: UInt64(remaining * 1_000_000_000)
                 )
             }

             reportRegistrationCompleteSubject.send(reportId)
         }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/Report/ViewModel/ReportRegistrationViewModel.swift
around lines 134–172, the catch block currently swallows errors and only sets
reportId to nil; update the catch to forward the caught error to
exceptionSubject (or a user-facing error subject) so the UI can display the
failure, then set reportId = nil (or keep as nil), and ensure you send a
user-friendly error value/object (or map the Error to one) to exceptionSubject
before completing the Task and sending reportRegistrationCompleteSubject.

Copy link
Contributor

@choijungp choijungp left a comment

Choose a reason for hiding this comment

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

현재는 그럼
제보하기 버튼 클릭 > LoadingView push !! > reportUseCase의 report 실행 (제보 api) > ReportRegistrationViewControllerDelegate의 completeRegistration 실행 (제보 완료 뷰로 넘어감)

요런 흐름이 맞나유 ?????
VC에게 Delegate를 위임하는 것은 또 첨인 것 같아가주구 신기하지만 완존 신박한 방법 !!!!

사실은 ~~ 딩 말씀처럼 루틴 등록 뷰 -> 로딩 뷰 -> 완료 뷰 이 과정이 있을 때
네이밍에 맞게 로딩 뷰에서 api를 쏴야한다고 생각했지만 데이터 넘기는 것을 생각하지 못했어유 !!

딩이 구현하신 방법이 현-명하다고 생각함니다 !!!!!!!

Comment on lines +14 to +16
case file1 = "additionalProp1"
case file2 = "additionalProp2"
case file3 = "additionalProp3"
Copy link
Contributor

Choose a reason for hiding this comment

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

요거 json이 "additionalProp1" 자체로 오는게 아니고 요청 값에 따라 달라지지 않나유 ? ?? ?

longitude: longitude,
latitude: latitude,
address: reportLocation),
thumbnailURL: reportImageUrl,
Copy link
Contributor

Choose a reason for hiding this comment

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

요거 thumbnailURL로 네이밍 바꾼거 넘 조은것 같아유 !!!!!!!
따봉앤명확 👍🏻

Comment on lines +39 to +45
public func report(
title: String,
content: String?,
category: ReportType,
location: LocationEntity?,
photos: [Data]
) async throws -> Int? {
Copy link
Contributor

Choose a reason for hiding this comment

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

제보하기 할 때 content가 nil인 경우가 있나용 ???
피그마에서는 전체가 필수 요소로 되어있는데 content와 location이 nil인 경우가 궁굼함니다 !!!

@choijungp choijungp merged commit 885a272 into develop Nov 23, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants