From aff99bd8805dee687e5aa84768f137ccf567174a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=B5=E1=86=AB?= Date: Mon, 17 Nov 2025 16:48:32 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20FloatingMenu=EC=97=90=20=EC=A0=9C?= =?UTF-8?q?=EB=B3=B4=ED=95=98=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#T3-197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/report_icon.imageset/Contents.json | 23 ++++ .../report_icon.imageset/report_icon.png | Bin 0 -> 607 bytes .../report_icon.imageset/report_icon@2x.png | Bin 0 -> 1057 bytes .../report_icon.imageset/report_icon@3x.png | Bin 0 -> 1547 bytes .../Common/Component/FloatingMenuView.swift | 102 ++++++++++++++---- .../Common/DesignSystem/BitnagilIcon.swift | 1 + .../Home/View/HomeViewController.swift | 7 +- .../RecommendedRoutineViewController.swift | 7 +- 8 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/Contents.json create mode 100644 Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@2x.png create mode 100644 Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@3x.png diff --git a/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/Contents.json b/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/Contents.json new file mode 100644 index 00000000..61b05e78 --- /dev/null +++ b/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "report_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "report_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "report_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon.png b/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..76ff07dfca4b9d53d357ecfd9b1c1cf8c752c631 GIT binary patch literal 607 zcmV-l0-*hgP)F;1A%0xaA8_ zu1H8&d;k~@Kp@#=yfFl`gf)-@JjveO@yxv0*%>eJ*g+T(RAcHhBtF2Q7(&g_-!Mw# zF764{_)>;){z1Xl;Y!6q)Oc1(zTbHVs_~s2%a1&(vzwx!&_#)YFuL^)1`$iYFBuYJ zL12=|hhO}7sVGs5_4?UZ8wVX9FUDbqO#HOnY;<3zv9a|VywAx|1p3^ZL#^1Fqx9NW+5x}70N{v9+MRw2X;FQcJ$ zNdV(dVMTh#Mx)W%dZ6m{B^>SUX+ckNSswSD?(R=w;`JvK9FJ4#AqTEaCIvyvjnvZ3 zRD2G+dDS1^y)9F)ScQWscj^omMl{s3(FX?Pswu)tZL8C{5KuELF$td?alTbNP~^EI zEd;JuF{7fIA!>$8G&^5`OBG%&bHlEHWcB<$xbb(*m&mq}YZ4V|{%<2EECo%bqqI6k tM9}?{IY`1BmvRfEhWa_>>5Jwe;tU4Jm11D_%=`cV002ovPDHLkV1f*O1M2_) literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@2x.png b/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6337def0bbd99ec056d2f9f514f5ed3aa120789c GIT binary patch literal 1057 zcmV++1m63JP)5gw^+h%vWZMWN5Y+`@OWOur=+xOp@_h#NKFk;024Pi(T`4to6$U?wE zI(0zBQZ=aE&)=$Kp75t$7i#!dykc{#4f;b(2G?*Edm-0C)*QFLkkeuY?cqRTpn{VxyUfL z&t<-GZZ(c(<9rKG9!%r(PbnZIM#na!4WFCUo&wl#`R98Lw(1xxaFzns};LLbHBEhBWC<~7+9}tEoAhcPh-o8*y=oo)z3R=)5z*bSO zMf>X&p2z~6UqB0W8(JXx;;se>s3H!=Gz~?fio4qWRI)8keKG|tXqyM@Uh{yl=&9-& zLe<~UL@xopY5y{w2`DlG&)n63A*9wZ4mxK~T|?1FfD_mETjz2f(1pGgw1+kUwE01% zY&)tV=Ygaq=hN1kwCk<~#x~CWb9`8U6mokKT1aYJje6)9oLNLg2}zAhAr=NDlZGBd z_oaj%`uM2=r#B7|m3z}H;5OnQR7OUXip7UH@tSwb&TEG_K5;I{;dT7W8Q(J8%Gq{; zqfL6Eild)fm`a&=cz;RTFbuK{eInMDPr{{$&L)(T%&Ya4$z=(Hvml3ev(ont}42a1<@XV26lq=E$Zmj7NvK;(J1ZS3NtcT1{r58OorRIk+7VGXs1 zYPlni=N}M}@WaR(_H+|H&$F=`t|1M+uhfO;0w`qeOu|Hr3?je^*)sYXDg@C5)Ka`d zOx=wmlsW8`5hIZWP>DZ#UMCl1tPkuB`C9@Th#76)$kcb;1UoG87Z~tIi1cK+)3@Cb bBZi2-%cK|~T$?*h00000NkvXXu0mjfakJah literal 0 HcmV?d00001 diff --git a/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@3x.png b/Projects/Presentation/Resources/Images.xcassets/Common/report_icon.imageset/report_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5ccda0360a029db29d62ebd23663043e6ebfd63f GIT binary patch literal 1547 zcmV+m2K4!fP)T<`XPY^GFTDcR2$wO znILqFX`K~eW0_uU`udU*>D}C2QQmKw-nK9y$nA$4IBmaxEsuPQqrXVquGQZ0dV3;T zklbD)KZ73-8qvhu%%Z6kP2mQkyVRS`Dm~pnBqZ(Uyg*M*`ro;+T}*zp{)O&Rs^#eD z6cN!an9Mh(1yNKg!V>o&<*H+lg;+g=w2Jh%)^EP{0%_+fb-2!x14d%lm^YcPwLm1! z5u{iJ2NwP^nXjEdPWIc`px#r8#kcrAuxOafkp+^)V&Ok8)z!Y^KL3Tb25pqI=@ZSv zi4w|wkG3V+ny#%P>h*^9GpB9JV?-Va_9in@{igkXYiKV=!fXJpZ-`EVwmROQFX6pe z&?EQ}O|f@)d%%hikT|2qIZ?PpB7OD=kYUb3x>ywZSI1?61d@-}3V8W?1J8f+pUQ`2 zW*Yc>kyRwIR~^v9_tE9o>#OL>WBl410?)g7yl#OF zb*$DISdb#zXkHHl-})PMRk>+*z=F`xbpSst=-+|hR0iyD`5U<$0}HlQGMPu6?O+*UA&#^s0NH7V6=LE{vua1+Vi)-#scG*Ov<9k>@85PWA?^b#tI2%-X&Cpa*!b{2<9Qh zpMQf5)=WBJ6!Qvu>`|=EcD$l4*7Y$&Aj8HFwv z6_(gNyiZdyD=@5rNCY*z!9~z=hv$Cws&#sxZl~w6lI7zCjGIvHSIvUbgK9}n;mnT4 zPvIl}sA(Yd$kP?6q@K&|FPCt3c8uR&x}o{fDbLPiY3xS&bv;70iij#>f75olY%W{C z{9FMa-sTrantMb%L-UJl16kX->ivBB0eVwKYFwqnQqEVEO|%21xw0X5G1=8i z>5AaTxVj^@6-ZQ~{TX3TtZ${MD^!6}j65v)Ez2qofd{5_({UDeo*}G2N`TXjDzB8) zOLyIJL%;QS?S#ZCA@VxJkt~^vaV8mcL}S@x)ak`uwXE|n6%<55(bJ=q#wL&x{$==x zCWdz-SEv$o10~ek2pT8G`fki=s`v?Ek^WnTx)@3G>*RRt5Q;&nqy?6)Gwt%tAl8pCi@ zv_p>Tx2Oab_R-KZLEwN|py!&W_=n0uJVJPu5hG&vyA$30D916(oYrYu877Y22NFPT zue}JD+?f|r#)Y1>P=$G_9D3dUH1La%0CH>nnEnQKx-Khj#o$_yOf^c%X9$Y`&&qnA zBqNYO4Po(^;T#tU31lUfQFD`wKp8u{K$4+S8Gu9ta+0o)<1!KZ7%LJ9P8nDg1VzZ2 zpL^LkNlYL;r{*!&Wd4G>hdok~nFxvB_3Ty$%g*gaUPBP-j>4v__IsY`qyVtq7A`kQ xtm#V UIButton { + let menuButton = UIButton() + let menuIconView = UIImageView() + let menuLabel = UILabel() + + menuIconView.image = menu.menuIcon + menuLabel.text = menu.menuLabel + menuLabel.font = BitnagilFont(style: .body2, weight: .medium).font + menuLabel.textColor = BitnagilColor.gray30 + + [menuIconView, menuLabel].forEach { + menuButton.addSubview($0) } - registerRoutineIconView.snp.makeConstraints { make in + menuIconView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview() - make.size.equalTo(Layout.registerRoutineIconSize) + make.size.equalTo(Layout.menuIconSize) } - registerRoutineLabel.snp.makeConstraints { make in - make.leading.equalTo(registerRoutineIconView.snp.trailing).offset(Layout.registerRoutineLabelLeadingSpacing) + menuLabel.snp.makeConstraints { make in + make.leading.equalTo(menuIconView.snp.trailing).offset(Layout.menuLabelLeadingSpacing) make.centerY.equalToSuperview() - make.height.equalTo(Layout.registerRoutineLabelHeight) + make.height.equalTo(Layout.menuLabelHeight) } + + return menuButton } } diff --git a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift index 314217fb..25cf9508 100644 --- a/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift +++ b/Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift @@ -15,6 +15,7 @@ enum BitnagilIcon { // MARK: - Common Icons static let backButtonIcon = UIImage(named: "back_button_icon", in: bundle, with: nil) static let plusIcon = UIImage(named: "plus_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate) + static let reportIcon = UIImage(named: "report_icon", in: bundle, with: nil) static let bitnagilChevronIcon = UIImage(named: "bitnagil_chevron_icon", in: bundle, with: nil) static func bitnagilChevronIcon(direction: Direction) -> UIImage? { return BitnagilIcon.bitnagilChevronIcon?.rotate(degrees: direction.rotation)?.withRenderingMode(.alwaysTemplate) diff --git a/Projects/Presentation/Sources/Home/View/HomeViewController.swift b/Projects/Presentation/Sources/Home/View/HomeViewController.swift index 6333b492..832135ff 100644 --- a/Projects/Presentation/Sources/Home/View/HomeViewController.swift +++ b/Projects/Presentation/Sources/Home/View/HomeViewController.swift @@ -45,7 +45,7 @@ final class HomeViewController: BaseViewController { static let floatingButtonBottomSpacing: CGFloat = 19 static let floatingButtonSize: CGFloat = 52 static let floatingMenuBottomSpacing: CGFloat = 16 - static let floatingMenuHeight: CGFloat = 56 + static let floatingMenuHeight: CGFloat = 104 static let floatingMenuWidth: CGFloat = 144 } @@ -664,6 +664,11 @@ extension HomeViewController: RoutineViewDelegate { // MARK: FloatingMenuViewDelegate extension HomeViewController: FloatingMenuViewDelegate { + func floatingMenuDidTapReportButton(_ sender: FloatingMenuView) { + toggleFloatingButton() + // TODO: 제보하기 뷰로 이동 + } + func floatingMenuDidTapRegisterRoutineButton(_ sender: FloatingMenuView) { toggleFloatingButton() guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { diff --git a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift index b5bb5cd5..17fbe427 100644 --- a/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift +++ b/Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift @@ -30,7 +30,7 @@ final class RecommendedRoutineViewController: BaseViewController Date: Tue, 18 Nov 2025 20:22:41 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Feat:=20ReportDetailView=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#T3-197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PresentationDependencyAssembler.swift | 4 + .../Home/View/HomeViewController.swift | 9 +- .../Sources/Report/Model/ReportDetail.swift | 15 ++ .../View/ReportDetailViewController.swift | 197 ++++++++++++++++++ .../ViewModel/ReportDetailViewModel.swift | 46 ++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 Projects/Presentation/Sources/Report/Model/ReportDetail.swift create mode 100644 Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift create mode 100644 Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift diff --git a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift index 06b4dd42..449a4542 100644 --- a/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift +++ b/Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift @@ -127,5 +127,9 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol { return ReportViewModel(reportUseCase: reportUseCase) } + + DIContainer.shared.register(type: ReportDetailViewModel.self) { container in + return ReportDetailViewModel() + } } } diff --git a/Projects/Presentation/Sources/Home/View/HomeViewController.swift b/Projects/Presentation/Sources/Home/View/HomeViewController.swift index 832135ff..1d586b0a 100644 --- a/Projects/Presentation/Sources/Home/View/HomeViewController.swift +++ b/Projects/Presentation/Sources/Home/View/HomeViewController.swift @@ -666,7 +666,14 @@ extension HomeViewController: RoutineViewDelegate { extension HomeViewController: FloatingMenuViewDelegate { func floatingMenuDidTapReportButton(_ sender: FloatingMenuView) { toggleFloatingButton() - // TODO: 제보하기 뷰로 이동 + // TODO: 제보하기 뷰로 이동 (현재는 제보 detailView) + guard let reportDetailViewModel = DIContainer.shared.resolve(type: ReportDetailViewModel.self) + else { fatalError("reportDetailViewModel 의존성이 등록되지 않았습니다.") } + + let reportDetailViewController = ReportDetailViewController(viewModel: reportDetailViewModel) + reportDetailViewController.hidesBottomBarWhenPushed = true + + self.navigationController?.pushViewController(reportDetailViewController, animated: true) } func floatingMenuDidTapRegisterRoutineButton(_ sender: FloatingMenuView) { diff --git a/Projects/Presentation/Sources/Report/Model/ReportDetail.swift b/Projects/Presentation/Sources/Report/Model/ReportDetail.swift new file mode 100644 index 00000000..8bb51325 --- /dev/null +++ b/Projects/Presentation/Sources/Report/Model/ReportDetail.swift @@ -0,0 +1,15 @@ +// +// ReportDetail.swift +// Presentation +// +// Created by 최정인 on 11/18/25. +// + + +struct ReportDetail { + let date: String + let title: String + let category: String + let description: String + let location: String +} diff --git a/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift b/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift new file mode 100644 index 00000000..f0e954a2 --- /dev/null +++ b/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift @@ -0,0 +1,197 @@ +// +// ReportDetailViewController.swift +// Presentation +// +// Created by 최정인 on 11/17/25. +// + +import Combine +import SnapKit +import UIKit + +class ReportDetailViewController: BaseViewController { + private enum Layout { + static let horizontalMargin: CGFloat = 20 + static let photoStackViewSpacing: CGFloat = 8 + static let photoStackViewTopSpacing: CGFloat = 14 + static let photoSize: CGFloat = 74 + static let contentStackViewSpacing: CGFloat = 28 + static let contentStackViewTopSpacing: CGFloat = 23 + static let reportStatusViewTopSpacing: CGFloat = 74 + 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 + } + + private enum ReportDetailContent: CaseIterable { + case title + case category + case description + case location + + var title: String { + switch self { + case .title: + return "제목" + case .category: + return "카테고리" + case .description: + return "상세 제보 내용" + case .location: + return "내 위치" + } + } + } + + private let reportStatusView = UIView() + private let dateLabel = UILabel() + private let photoStackView = UIStackView() + private let contentStackView = UIStackView() + private let titleContentLabel = UILabel() + private let categoryLabel = UILabel() + private let detailDescriptionLabel = UILabel() + private let locationLabel = UILabel() + private var cancellables: Set = [] + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.action(input: .fetchReportDetail) + } + + override func configureAttribute() { + view.backgroundColor = .white + configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "제보하기")) + + // 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 + + photoStackView.axis = .horizontal + photoStackView.spacing = Layout.photoStackViewSpacing + + contentStackView.axis = .vertical + contentStackView.spacing = Layout.contentStackViewSpacing + + ReportDetailContent.allCases.forEach { reportDetailContent in + let reportContentView = makeReportContentView(reportDetailContentType: reportDetailContent) + contentStackView.addArrangedSubview(reportContentView) + } + } + + override func configureLayout() { + let safeArea = view.safeAreaLayoutGuide + + view.addSubview(reportStatusView) + view.addSubview(dateLabel) + view.addSubview(photoStackView) + view.addSubview(contentStackView) + + reportStatusView.snp.makeConstraints { make in + make.top.equalTo(safeArea).offset(Layout.reportStatusViewTopSpacing) + make.width.equalTo(Layout.reportStatusViewWidth) + make.height.equalTo(Layout.reportStatusViewHeight) + make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + } + + dateLabel.snp.makeConstraints { make in + make.top.equalTo(reportStatusView.snp.bottom).offset(Layout.dateLabelTopSpacing) + make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + } + + photoStackView.snp.makeConstraints { make in + make.top.equalTo(dateLabel.snp.bottom).offset(Layout.photoStackViewTopSpacing) + make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + } + + contentStackView.snp.makeConstraints { make in + make.top.equalTo(photoStackView.snp.bottom).offset(Layout.contentStackViewTopSpacing) + make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin) + } + } + + override func bind() { + viewModel.output.reportDetailPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] reportDetail in + self?.fillReportContent(reportDetail: reportDetail) + } + .store(in: &cancellables) + } + + private func makeReportContentView(reportDetailContentType: ReportDetailContent) -> UIView { + let reportContentView = UIView() + let titleLabel = UILabel() + let backgroudView = UIView() + + titleLabel.text = reportDetailContentType.title + titleLabel.font = BitnagilFont(style: .body2, weight: .semiBold).font + titleLabel.textColor = BitnagilColor.gray10 + + backgroudView.layer.masksToBounds = true + backgroudView.layer.cornerRadius = 12 + backgroudView.backgroundColor = BitnagilColor.gray99 + + var descriptionLabel: UILabel + switch reportDetailContentType { + case .title: + descriptionLabel = titleContentLabel + case .category: + descriptionLabel = categoryLabel + case .description: + descriptionLabel = detailDescriptionLabel + descriptionLabel.numberOfLines = 0 + case .location: + descriptionLabel = locationLabel + } + descriptionLabel.font = BitnagilFont(style: .body2, weight: .medium).font + descriptionLabel.textColor = BitnagilColor.gray10 + + backgroudView.addSubview(descriptionLabel) + [titleLabel, backgroudView].forEach { + reportContentView.addSubview($0) + } + + titleLabel.snp.makeConstraints { make in + make.top.leading.equalToSuperview() + make.height.equalTo(Layout.horizontalMargin) + } + + backgroudView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(Layout.reportContentBackgroudViewTopSpacing) + make.horizontalEdges.equalToSuperview() + make.bottom.equalToSuperview() + } + + descriptionLabel.snp.makeConstraints { make in + make.verticalEdges.equalToSuperview().inset(Layout.reportContentDescriptionVerticalMargin) + make.horizontalEdges.equalToSuperview().inset(Layout.horizontalMargin) + } + + return reportContentView + } + + 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) + + dateLabel.text = reportDetail.date + titleContentLabel.text = reportDetail.title + categoryLabel.text = reportDetail.category + detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: reportDetail.description) + locationLabel.text = reportDetail.location + } +} diff --git a/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift new file mode 100644 index 00000000..440b0582 --- /dev/null +++ b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift @@ -0,0 +1,46 @@ +// +// ReportDetailViewModel.swift +// Presentation +// +// Created by 최정인 on 11/18/25. +// + +import Combine +import Domain + +final class ReportDetailViewModel: ViewModel { + enum Input { + case fetchReportDetail + } + + struct Output { + let reportDetailPublisher: AnyPublisher + } + + private(set) var output: Output + private let reportDetailSubject = CurrentValueSubject(nil) + + init() { + self.output = Output( + reportDetailPublisher: reportDetailSubject.eraseToAnyPublisher() + ) + } + + func action(input: Input) { + switch input { + case .fetchReportDetail: + fetchReportDetail() + } + } + + private func fetchReportDetail() { + let report = ReportDetail( + date: "2025.11.03 (금)", + title: "가로등이 깜빡거려요.", + category: "교통 시설", + description: "가로등이 깜박거리고 치지직 거려서 곧 터질 것 같아요... 햇살이 유리창 너머로 스며들며 방 안을 부드럽게 채운다. 커피 향이 퍼지고, 어제의 고민이 조금은 멀게 느껴진다. 오늘은 완벽하지 않아도 괜찮다. 천천히 숨을 고르고, 다시 한 걸음 내딛으면 된다. 이게 뭐람.", + location: "서울특별시 강남구 삼성동") + + reportDetailSubject.send(report) + } +} From 5da662eca8541391b57247c8e4bacdfb24e1a155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=B5=E1=86=AB?= Date: Tue, 18 Nov 2025 21:48:44 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Fix:=20ScrollView=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20ReportType=EC=9C=BC=EB=A1=9C=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Report/Model/ReportDetail.swift | 3 +- .../View/ReportDetailViewController.swift | 34 +++++++++++++------ .../ViewModel/ReportDetailViewModel.swift | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Projects/Presentation/Sources/Report/Model/ReportDetail.swift b/Projects/Presentation/Sources/Report/Model/ReportDetail.swift index 8bb51325..be6c1498 100644 --- a/Projects/Presentation/Sources/Report/Model/ReportDetail.swift +++ b/Projects/Presentation/Sources/Report/Model/ReportDetail.swift @@ -5,11 +5,12 @@ // Created by 최정인 on 11/18/25. // +import Domain struct ReportDetail { let date: String let title: String - let category: String + let category: ReportType let description: String let location: String } diff --git a/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift b/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift index f0e954a2..1983526a 100644 --- a/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift +++ b/Projects/Presentation/Sources/Report/View/ReportDetailViewController.swift @@ -45,6 +45,8 @@ class ReportDetailViewController: BaseViewController { } } + private let scrollView = UIScrollView() + private let contentView = UIView() private let reportStatusView = UIView() private let dateLabel = UILabel() private let photoStackView = UIStackView() @@ -64,6 +66,7 @@ class ReportDetailViewController: BaseViewController { view.backgroundColor = .white configureCustomNavigationBar(navigationBarStyle: .withBackButton(title: "제보하기")) + scrollView.showsVerticalScrollIndicator = false // TODO: 추후 공통 component로 교체 reportStatusView.layer.masksToBounds = true reportStatusView.layer.cornerRadius = 6 @@ -87,31 +90,42 @@ class ReportDetailViewController: BaseViewController { override func configureLayout() { let safeArea = view.safeAreaLayoutGuide - view.addSubview(reportStatusView) - view.addSubview(dateLabel) - view.addSubview(photoStackView) - view.addSubview(contentStackView) + view.addSubview(scrollView) + scrollView.addSubview(contentView) + [reportStatusView, dateLabel, photoStackView, contentStackView].forEach { + contentView.addSubview($0) + } + + scrollView.snp.makeConstraints { make in + make.edges.equalTo(safeArea) + } + + contentView.snp.makeConstraints { make in + make.edges.equalTo(scrollView.contentLayoutGuide) + make.width.equalTo(scrollView.frameLayoutGuide) + } reportStatusView.snp.makeConstraints { make in - make.top.equalTo(safeArea).offset(Layout.reportStatusViewTopSpacing) + make.top.equalTo(contentView).offset(Layout.reportStatusViewTopSpacing) make.width.equalTo(Layout.reportStatusViewWidth) make.height.equalTo(Layout.reportStatusViewHeight) - make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + make.leading.equalTo(contentView).offset(Layout.horizontalMargin) } dateLabel.snp.makeConstraints { make in make.top.equalTo(reportStatusView.snp.bottom).offset(Layout.dateLabelTopSpacing) - make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + make.leading.equalTo(contentView).offset(Layout.horizontalMargin) } photoStackView.snp.makeConstraints { make in make.top.equalTo(dateLabel.snp.bottom).offset(Layout.photoStackViewTopSpacing) - make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) + make.leading.equalTo(contentView).offset(Layout.horizontalMargin) } contentStackView.snp.makeConstraints { make in make.top.equalTo(photoStackView.snp.bottom).offset(Layout.contentStackViewTopSpacing) - make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin) + make.horizontalEdges.equalTo(contentView).inset(Layout.horizontalMargin) + make.bottom.equalToSuperview().offset(-40) } } @@ -190,7 +204,7 @@ class ReportDetailViewController: BaseViewController { dateLabel.text = reportDetail.date titleContentLabel.text = reportDetail.title - categoryLabel.text = reportDetail.category + categoryLabel.text = reportDetail.category.name detailDescriptionLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: reportDetail.description) locationLabel.text = reportDetail.location } diff --git a/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift index 440b0582..5d61fee7 100644 --- a/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift +++ b/Projects/Presentation/Sources/Report/ViewModel/ReportDetailViewModel.swift @@ -37,7 +37,7 @@ final class ReportDetailViewModel: ViewModel { let report = ReportDetail( date: "2025.11.03 (금)", title: "가로등이 깜빡거려요.", - category: "교통 시설", + category: .water, description: "가로등이 깜박거리고 치지직 거려서 곧 터질 것 같아요... 햇살이 유리창 너머로 스며들며 방 안을 부드럽게 채운다. 커피 향이 퍼지고, 어제의 고민이 조금은 멀게 느껴진다. 오늘은 완벽하지 않아도 괜찮다. 천천히 숨을 고르고, 다시 한 걸음 내딛으면 된다. 이게 뭐람.", location: "서울특별시 강남구 삼성동") From 34c9d8b95e1ed2f29d7a4b9d701d181c62163feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=B5=E1=86=AB?= Date: Tue, 18 Nov 2025 21:54:52 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Feat:=20Xcode=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20Tuist=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SupportingFiles/Info.plist | 22 +++++++++++----------- Tuist.swift | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/SupportingFiles/Info.plist b/SupportingFiles/Info.plist index 3becccad..43523208 100644 --- a/SupportingFiles/Info.plist +++ b/SupportingFiles/Info.plist @@ -2,14 +2,6 @@ - NSLocationWhenInUseUsageDescription - 위치 정보 사용을 허용하시겠습니까? - LSApplicationCategoryType - - NSPhotoLibraryUsageDescription - 앨범에서 사진을 선택합니다. 허용하시겠습니까? - NSCameraUsageDescription - 카메라를 사용해 사진을 촬영합니다. 허용하시겠습니까? BaseURL $(BASE_URL) CFBundleExecutable @@ -23,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 2.0.0 CFBundleURLTypes @@ -37,15 +29,23 @@ CFBundleVersion 1 - KakaoNativeKey - $(KAKAO_NATIVE_KEY) KakaoAPIKey $(KAKAO_API_KEY) + KakaoNativeKey + $(KAKAO_NATIVE_KEY) + LSApplicationCategoryType + LSApplicationQueriesSchemes kakaokompassauth itms-apps + NSCameraUsageDescription + 카메라를 사용해 사진을 촬영합니다. 허용하시겠습니까? + NSLocationWhenInUseUsageDescription + 위치 정보 사용을 허용하시겠습니까? + NSPhotoLibraryUsageDescription + 앨범에서 사진을 선택합니다. 허용하시겠습니까? UIAppFonts Pretendard-Bold.otf diff --git a/Tuist.swift b/Tuist.swift index f6b0a350..c3ea6aff 100644 --- a/Tuist.swift +++ b/Tuist.swift @@ -2,6 +2,6 @@ import ProjectDescription let tuist = Tuist( project: .tuist( - compatibleXcodeVersions: .upToNextMajor("16.0") + compatibleXcodeVersions: .all ) ) From 6ea3423e97c62aa068d48e6e02aa4a49e39ddcc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B?= =?UTF-8?q?=E1=85=B5=E1=86=AB?= Date: Tue, 18 Nov 2025 22:49:09 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Fix:=20Tuist=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tuist.swift b/Tuist.swift index c3ea6aff..f6b0a350 100644 --- a/Tuist.swift +++ b/Tuist.swift @@ -2,6 +2,6 @@ import ProjectDescription let tuist = Tuist( project: .tuist( - compatibleXcodeVersions: .all + compatibleXcodeVersions: .upToNextMajor("16.0") ) )