-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat-T3-202] 제보 상세화면 API 연동 #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // | ||
| // ReportDTO.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 11/20/25. | ||
| // | ||
|
|
||
| import Domain | ||
|
|
||
| struct ReportDTO: Decodable { | ||
| let reportId: Int? | ||
| let reportDate: String? | ||
| let reportTitle: String | ||
| let reportContent: String? | ||
| let reportLocation: String | ||
| let reportStatus: String | ||
| let reportCategory: String | ||
| let reportImageUrl: String? | ||
| let reportImageUrls: [String]? | ||
| let latitude: Double? | ||
| let longitude: Double? | ||
|
|
||
| func toReportEntity() throws -> ReportEntity { | ||
| guard let reportId else { throw NetworkError.decodingError } | ||
| return ReportEntity( | ||
| id: reportId, | ||
| title: reportTitle, | ||
| date: reportDate, | ||
| type: ReportType(rawValue: reportCategory) ?? .transportation, | ||
| progress: ReportProgress(rawValue: reportStatus) ?? .received, | ||
| content: reportContent, | ||
| location: LocationEntity( | ||
| longitude: longitude, | ||
| latitude: latitude, | ||
| address: reportLocation), | ||
| photoUrls: reportImageUrls ?? []) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // | ||
| // ReportEndpoint.swift | ||
| // DataSource | ||
| // | ||
| // Created by 최정인 on 11/20/25. | ||
| // | ||
|
|
||
| enum ReportEndpoint { | ||
| case fetchReportDetail(reportId: Int) | ||
| } | ||
|
|
||
| extension ReportEndpoint: Endpoint { | ||
| var baseURL: String { | ||
| switch self { | ||
| case .fetchReportDetail: | ||
| return AppProperties.baseURL + "/api/v2/reports" | ||
| } | ||
| } | ||
|
|
||
| var path: String { | ||
| switch self { | ||
| case .fetchReportDetail(let reportId): | ||
| "\(baseURL)/\(reportId)" | ||
| } | ||
| } | ||
|
|
||
| var method: HTTPMethod { | ||
| switch self { | ||
| case .fetchReportDetail: | ||
| .get | ||
| } | ||
| } | ||
|
|
||
| var headers: [String : String] { | ||
| let headers: [String: String] = [ | ||
| "Content-Type": "application/json", | ||
| "accept": "*/*" | ||
| ] | ||
| return headers | ||
| } | ||
|
|
||
| var queryParameters: [String : String] { | ||
| return [:] | ||
| } | ||
|
|
||
| var bodyParameters: [String : Any] { | ||
| return [:] | ||
| } | ||
|
|
||
| var isAuthorized: Bool { | ||
| return true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Combine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Kingfisher | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import SnapKit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import UIKit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,8 +21,6 @@ class ReportDetailViewController: BaseViewController<ReportDetailViewModel> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let contentStackViewTopSpacing: CGFloat = 23 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let contentStackViewBottomSpacing: CGFloat = 40 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let reportStatusViewTopSpacing: CGFloat = 20 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let reportStatusViewWidth: CGFloat = 65 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let reportStatusViewHeight: CGFloat = 26 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let dateLabelTopSpacing: CGFloat = 6 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let reportContentBackgroudViewTopSpacing: CGFloat = 8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let reportContentDescriptionVerticalMargin: CGFloat = 16 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -42,14 +41,14 @@ class ReportDetailViewController: BaseViewController<ReportDetailViewModel> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "상세 제보 내용" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .location: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "내 위치" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "신고 위치" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let scrollView = UIScrollView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let contentView = UIView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let reportStatusView = UIView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let reportStatusView = ReportProgressView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let dateLabel = UILabel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let photoStackView = UIStackView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let contentStackView = UIStackView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,21 +57,27 @@ class ReportDetailViewController: BaseViewController<ReportDetailViewModel> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let detailDescriptionLabel = UILabel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let locationLabel = UILabel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var cancellables: Set<AnyCancellable> = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let reportId: Int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| init(viewModel: ReportDetailViewModel, reportId: Int) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.reportId = reportId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super.init(viewModel: viewModel) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required init?(coder: NSCoder) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fatalError("init(coder:) has not been implemented") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override func viewDidLoad() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super.viewDidLoad() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewModel.action(input: .fetchReportDetail) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewModel.action(input: .fetchReportDetail(reportId: reportId)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override func configureAttribute() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| view.backgroundColor = .white | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "제보하기")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollView.showsVerticalScrollIndicator = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: 추후 공통 component로 교체 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportStatusView.layer.masksToBounds = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportStatusView.layer.cornerRadius = 6 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportStatusView.backgroundColor = BitnagilColor.green10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dateLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dateLabel.textColor = BitnagilColor.gray10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -110,8 +115,6 @@ class ReportDetailViewController: BaseViewController<ReportDetailViewModel> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportStatusView.snp.makeConstraints { make in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.top.equalTo(contentView).offset(Layout.reportStatusViewTopSpacing) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.width.equalTo(Layout.reportStatusViewWidth) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.height.equalTo(Layout.reportStatusViewHeight) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.leading.equalTo(contentView).offset(Layout.horizontalMargin) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -196,19 +199,26 @@ class ReportDetailViewController: BaseViewController<ReportDetailViewModel> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func fillReportContent(reportDetail: ReportDetail?) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let reportDetail else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let photoView = UIView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.backgroundColor = BitnagilColor.gray30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.layer.masksToBounds = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.layer.cornerRadius = 9.25 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.snp.makeConstraints { make in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.size.equalTo(Layout.photoSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoStackView.addArrangedSubview(photoView) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportStatusView.configure(with: reportDetail.status) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dateLabel.text = reportDetail.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| titleContentLabel.text = reportDetail.title | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| categoryLabel.text = reportDetail.category.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: reportDetail.description) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .attributedString(text: reportDetail.description) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| locationLabel.text = reportDetail.location | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reportDetail.photoUrls.forEach { photoUrl in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let photoView = UIImageView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let url = URL(string: photoUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.kf.setImage(with: url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.layer.masksToBounds = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.layer.cornerRadius = 9.25 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoView.snp.makeConstraints { make in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| make.size.equalTo(Layout.photoSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoStackView.addArrangedSubview(photoView) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
199
to
223
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
현재 구현에는 두 가지 우려가 있습니다.
아래처럼 기존 뷰를 먼저 비우고, private func fillReportContent(reportDetail: ReportDetail?) {
guard let reportDetail else { return }
@@
detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium)
.attributedString(text: reportDetail.description)
locationLabel.text = reportDetail.location
-
- reportDetail.photoUrls.forEach { photoUrl in
- let photoView = UIImageView()
-
- if let url = URL(string: photoUrl) {
- photoView.kf.setImage(with: url)
- }
- photoView.layer.masksToBounds = true
- photoView.layer.cornerRadius = 9.25
- photoView.snp.makeConstraints { make in
- make.size.equalTo(Layout.photoSize)
- }
- photoStackView.addArrangedSubview(photoView)
- }
+ // 기존 이미지 정리
+ photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
+
+ reportDetail.photoUrls.forEach { photoUrl in
+ let photoView = UIImageView()
+ photoView.layer.masksToBounds = true
+ photoView.layer.cornerRadius = 9.25
+ photoView.contentMode = .scaleAspectFill
+
+ if let url = URL(string: photoUrl) {
+ photoView.kf.setImage(with: url)
+ }
+
+ photoStackView.addArrangedSubview(photoView)
+ photoView.snp.makeConstraints { make in
+ make.size.equalTo(Layout.photoSize)
+ }
+ }이제 파일의 실제 코드를 검증하겠습니다. 실제 코드를 확인하기 위해 코드베이스를 검색하겠습니다. 🏁 Script executed: #!/bin/bash
# ReportDetailViewController 파일 내용 확인
find . -name "ReportDetailViewController.swift" -type f -exec cat {} \;Length of output: 8204 이제 UIStackView와 SnapKit의 제약 설정 순서에 대해 기술적으로 검증하겠습니다. 이미지 뷰 중복 추가와 SnapKit 제약 순서 개선이 필요합니다 검증 결과, 현재 구현의 두 가지 문제가 확인되었습니다:
아래와 같이 기존 뷰를 먼저 비우고, private func fillReportContent(reportDetail: ReportDetail?) {
guard let reportDetail else { return }
@@
detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium)
.attributedString(text: reportDetail.description)
locationLabel.text = reportDetail.location
-
- reportDetail.photoUrls.forEach { photoUrl in
- let photoView = UIImageView()
-
- if let url = URL(string: photoUrl) {
- photoView.kf.setImage(with: url)
- }
- photoView.layer.masksToBounds = true
- photoView.layer.cornerRadius = 9.25
- photoView.snp.makeConstraints { make in
- make.size.equalTo(Layout.photoSize)
- }
- photoStackView.addArrangedSubview(photoView)
- }
+ // 기존 이미지 정리
+ photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
+
+ reportDetail.photoUrls.forEach { photoUrl in
+ let photoView = UIImageView()
+ photoView.layer.masksToBounds = true
+ photoView.layer.cornerRadius = 9.25
+ photoView.contentMode = .scaleAspectFill
+
+ if let url = URL(string: photoUrl) {
+ photoView.kf.setImage(with: url)
+ }
+
+ photoStackView.addArrangedSubview(photoView)
+ photoView.snp.makeConstraints { make in
+ make.size.equalTo(Layout.photoSize)
+ }
+ }🏁 Script executed: #!/bin/bash
# ReportDetailViewController의 viewModel 동작 확인
# bind() 메서드와 publisher 호출 패턴 확인
grep -n "reportDetailPublisher" . -r --include="*.swift" -B2 -A2Length of output: 1648 🌐 Web query: 💡 Result: Short answer
Why
Minimal example (recommended pattern)
let stack = UIStackView() let card = UIView() // constrain card's internal content (preferred) // if you must constrain card's size, do it after adding: Notes / pitfalls
Sources
이미지 뷰 중복 추가와 SnapKit 제약 순서 개선이 필요합니다 검증 결과, 현재 구현의 두 가지 문제가 모두 확인되었습니다:
아래와 같이 기존 뷰를 먼저 비우고, private func fillReportContent(reportDetail: ReportDetail?) {
guard let reportDetail else { return }
@@
detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium)
.attributedString(text: reportDetail.description)
locationLabel.text = reportDetail.location
-
- reportDetail.photoUrls.forEach { photoUrl in
- let photoView = UIImageView()
-
- if let url = URL(string: photoUrl) {
- photoView.kf.setImage(with: url)
- }
- photoView.layer.masksToBounds = true
- photoView.layer.cornerRadius = 9.25
- photoView.snp.makeConstraints { make in
- make.size.equalTo(Layout.photoSize)
- }
- photoStackView.addArrangedSubview(photoView)
- }
+ // 기존 이미지 정리
+ photoStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
+
+ reportDetail.photoUrls.forEach { photoUrl in
+ let photoView = UIImageView()
+ photoView.layer.masksToBounds = true
+ photoView.layer.cornerRadius = 9.25
+ photoView.contentMode = .scaleAspectFill
+
+ if let url = URL(string: photoUrl) {
+ photoView.kf.setImage(with: url)
+ }
+
+ photoStackView.addArrangedSubview(photoView)
+ photoView.snp.makeConstraints { make in
+ make.size.equalTo(Layout.photoSize)
+ }
+ }📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ import Domain | |
|
|
||
| final class ReportDetailViewModel: ViewModel { | ||
| enum Input { | ||
| case fetchReportDetail | ||
| case fetchReportDetail(reportId: Int) | ||
| } | ||
|
|
||
| struct Output { | ||
|
|
@@ -19,28 +19,40 @@ final class ReportDetailViewModel: ViewModel { | |
|
|
||
| private(set) var output: Output | ||
| private let reportDetailSubject = CurrentValueSubject<ReportDetail?, Never>(nil) | ||
| private let reportRepository: ReportRepositoryProtocol | ||
|
|
||
| init() { | ||
| init(reportRepository: ReportRepositoryProtocol) { | ||
| self.reportRepository = reportRepository | ||
| self.output = Output( | ||
| reportDetailPublisher: reportDetailSubject.eraseToAnyPublisher() | ||
| ) | ||
| } | ||
|
|
||
| func action(input: Input) { | ||
| switch input { | ||
| case .fetchReportDetail: | ||
| fetchReportDetail() | ||
| case .fetchReportDetail(let reportId): | ||
| fetchReportDetail(reportId: reportId) | ||
| } | ||
| } | ||
|
|
||
| private func fetchReportDetail() { | ||
| let report = ReportDetail( | ||
| date: "2025.11.03 (금)", | ||
| title: "가로등이 깜빡거려요.", | ||
| category: .water, | ||
| description: "가로등이 깜박거리고 치지직 거려서 곧 터질 것 같아요... 햇살이 유리창 너머로 스며들며 방 안을 부드럽게 채운다. 커피 향이 퍼지고, 어제의 고민이 조금은 멀게 느껴진다. 오늘은 완벽하지 않아도 괜찮다. 천천히 숨을 고르고, 다시 한 걸음 내딛으면 된다. 이게 뭐람.", | ||
| location: "서울특별시 강남구 삼성동") | ||
|
|
||
| reportDetailSubject.send(report) | ||
| private func fetchReportDetail(reportId: Int) { | ||
| Task { | ||
| do { | ||
| if let reportEntity = try await reportRepository.fetchReportDetail(reportId: reportId) { | ||
| let reportDetail = ReportDetail( | ||
| date: reportEntity.date ?? "", | ||
| title: reportEntity.title, | ||
| status: reportEntity.progress, | ||
| category: reportEntity.type, | ||
| description: reportEntity.content ?? "", | ||
| location: reportEntity.location.address ?? "", | ||
| photoUrls: reportEntity.photoUrls) | ||
| reportDetailSubject.send(reportDetail) | ||
| } | ||
| reportDetailSubject.send(nil) | ||
| } catch { | ||
| reportDetailSubject.send(nil) | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+38
to
57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
현재 구현에는 두 가지 문제가 있습니다.
아래처럼 성공/실패/데이터없음 케이스를 명확히 나누고, - private func fetchReportDetail(reportId: Int) {
- Task {
- do {
- if let reportEntity = try await reportRepository.fetchReportDetail(reportId: reportId) {
- let reportDetail = ReportDetail(
- date: reportEntity.date ?? "",
- title: reportEntity.title,
- status: reportEntity.progress,
- category: reportEntity.type,
- description: reportEntity.content ?? "",
- location: reportEntity.location.address ?? "",
- photoUrls: reportEntity.photoUrls)
- reportDetailSubject.send(reportDetail)
- }
- reportDetailSubject.send(nil)
- } catch {
- reportDetailSubject.send(nil)
- }
- }
- }
+ private func fetchReportDetail(reportId: Int) {
+ Task { [weak self] in
+ guard let self else { return }
+
+ do {
+ if let reportEntity = try await self.reportRepository.fetchReportDetail(reportId: reportId) {
+ let reportDetail = ReportDetail(
+ date: reportEntity.date ?? "",
+ title: reportEntity.title,
+ status: reportEntity.progress,
+ category: reportEntity.type,
+ description: reportEntity.content ?? "",
+ location: reportEntity.location.address ?? "",
+ photoUrls: reportEntity.photoUrls)
+ self.reportDetailSubject.send(reportDetail)
+ } else {
+ self.reportDetailSubject.send(nil)
+ }
+ } catch {
+ self.reportDetailSubject.send(nil)
+ }
+ }
+ }이렇게 하면:
🏁 Script executed: #!/bin/bash
# Find ReportDetailViewModel.swift and verify the code
find . -name "ReportDetailViewModel.swift" -type f 2>/dev/null | head -5Length of output: 76 🏁 Script executed: #!/bin/bash
# Extract and display the fetchReportDetail method around lines 38-57
if [ -f "Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift" ]; then
sed -n '38,57p' "Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift"
else
echo "File not found at expected path"
fiLength of output: 871 🏁 Script executed: #!/bin/bash
# Search for ReportDetailViewController and fillReportContent method
find . -name "ReportDetailViewController.swift" -type f 2>/dev/nullLength of output: 76 🏁 Script executed: #!/bin/bash
# Check reportDetailSubject type and usage
rg -n "reportDetailSubject" "Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift" -A 2 -B 2 2>/dev/null || echo "File not found or no matches"Length of output: 761 🏁 Script executed: #!/bin/bash
# Look for fillReportContent in ReportDetailViewController
rg -n "fillReportContent\|reportDetailSubject" "Projects/Presentation/Sources/Report/View/Controller/" -A 3 -B 1 2>/dev/null | head -30Length of output: 0 Length of output: 661 🏁 Script executed: #!/bin/bash
# Search for reportDetailPublisher subscription in ReportDetailViewController.swift
rg -n "reportDetailPublisher\|\.sink\|\.assign\|\.receive" "Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift" -B 2 -A 3Length of output: 0 🏁 Script executed: #!/bin/bash
# Get the whole ReportDetailViewController to understand how reportDetailPublisher is used
cat "Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift" | head -100Length of output: 3298
현재 구현에는 두 가지 문제가 있습니다.
- private func fetchReportDetail(reportId: Int) {
- Task {
- do {
- if let reportEntity = try await reportRepository.fetchReportDetail(reportId: reportId) {
- let reportDetail = ReportDetail(
- date: reportEntity.date ?? "",
- title: reportEntity.title,
- status: reportEntity.progress,
- category: reportEntity.type,
- description: reportEntity.content ?? "",
- location: reportEntity.location.address ?? "",
- photoUrls: reportEntity.photoUrls)
- reportDetailSubject.send(reportDetail)
- }
- reportDetailSubject.send(nil)
- } catch {
- reportDetailSubject.send(nil)
- }
- }
- }
+ private func fetchReportDetail(reportId: Int) {
+ Task { [weak self] in
+ guard let self else { return }
+
+ do {
+ if let reportEntity = try await self.reportRepository.fetchReportDetail(reportId: reportId) {
+ let reportDetail = ReportDetail(
+ date: reportEntity.date ?? "",
+ title: reportEntity.title,
+ status: reportEntity.progress,
+ category: reportEntity.type,
+ description: reportEntity.content ?? "",
+ location: reportEntity.location.address ?? "",
+ photoUrls: reportEntity.photoUrls)
+ self.reportDetailSubject.send(reportDetail)
+ } else {
+ self.reportDetailSubject.send(nil)
+ }
+ } catch {
+ self.reportDetailSubject.send(nil)
+ }
+ }
+ }🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔티티 변환 실패 가능성을 염두에 두셨군요!
추후에 다른 DTO -> Entity 에도 throw를 할 수 있게 하고, 따로 Error를 정의해서 로그를 찍는것도 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
조아유 !!!!!!!!!