Repository navigation
[Feature/#186] 보유와인 정보 수정화면 > 빈티지 적용 - #187
Conversation
f7c330a to
bf68ca8
Compare
📝 WalkthroughSummary by CodeRabbit
Walkthrough빈티지(연도) 선택 기능을 UI 전반에 연동하고 보유 와인 수정 화면을 재구성했습니다. 기존 ChangeMyOwnedWineView를 삭제하고 ChangeMyWineView로 교체했으며, 뷰모델·컨트롤러·YearPicker API와 텍스트 스타일링 및 일부 레이아웃을 정리·조정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor 사용자
participant EditVC as ChangeMyOwnedWineViewController
participant EditView as ChangeMyWineView
participant YearModal as YearPickerModalVC
participant API as UpdateAPI
사용자->>EditView: 빈티지 라벨 탭
EditView->>EditVC: onLabelTapped 콜백
EditVC->>YearModal: present (pageSheet)
YearModal-->>EditVC: onYearConfirmed(year)
EditVC->>EditView: yearPicker.setInitialYear(year)
사용자->>EditView: 저장 버튼 탭
EditView->>EditVC: completeEdit()
alt vintage 없음
EditVC-->>사용자: 토스트 표시(빈티지 필요)
else vintage 있음
EditVC->>API: callUpdateAPI(wineId, price, vintage, buyDate)
API-->>EditVC: 성공
EditVC-->>사용자: 차단뷰 해제 후 pop (지연 1s)
end
sequenceDiagram
autonumber
actor 사용자
participant InfoVC as MyOwnedWineInfoViewController
participant Backend as FetchMyWineAPI
사용자->>InfoVC: 화면 진입
InfoVC->>Backend: fetchMyWineAPI()
Backend-->>InfoVC: 데이터 반환
InfoVC->>InfoVC: registerWine 갱신 (비동기)
InfoVC->>InfoVC: setWineData() on Main (header uses getDisplayedName())
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧠 Learnings (2)📓 Common learnings📚 Learning: 2025-09-13T09:09:45.616ZApplied to files:
🔇 Additional comments (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/MyOwnedWineInfoViewController.swift (2)
165-179: 강제 언래핑(!)로 인한 크래시 위험.registerWine가 nil일 경우 즉시 크래시합니다(네트워크/전환 타이밍 이슈 포함). 안전하게 가드 처리하세요.
- let data = try await networkService.fetchMyWine(myWineId: registerWine!.myWineId) + guard let id = registerWine?.myWineId else { return } + let data = try await networkService.fetchMyWine(myWineId: id)
161-171: 삭제 API 호출부도 강제 언래핑 제거.동일한 NPE 위험이 있으니 가드 처리로 통일하세요.
- _ = try await networkService.deleteMyWine(myWineId: registerWine!.myWineId) + guard let id = registerWine?.myWineId else { return } + _ = try await networkService.deleteMyWine(myWineId: id)DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (2)
146-170: 삭제 플로우가 API 호출 없이 화면만 닫힘현재 삭제 액션에서 API를 호출하지 않고 pop만 합니다. 또한 빈티지 미보유 시 삭제 자체가 불가능합니다(불필요한
guard vintage). API 성공 시에만 pop/hide 되도록 수정해 주세요.@objc private func deleteNewWine() { guard let currentWine = self.registerWine else { return } - guard let vintage = currentWine.getVintage() else { return } let alert = UIAlertController( title: "이 와인을 삭제하시겠습니까?", - message: "\(currentWine.wineName) \(vintage)", + message: "\(currentWine.wineName) \((currentWine.getVintage().map { "\($0)" }) ?? "NV")", preferredStyle: .alert ) @@ alert.addAction(UIAlertAction(title: "삭제", style: .destructive, handler: { [weak self] _ in guard let self = self else { return } self.logButtonClick(screenName: screenName, buttonName: Tracking.ButtonEvent.deleteBtnTapped, fileName: #file) - self.view.showBlockingView() - - DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) - } + self.callDeleteAPI() }))private func callDeleteAPI() { - guard let wine = registerWine else {return} - - self.view.showBlockingView() - Task { - do { - _ = try await networkService.deleteMyWine(myWineId: wine.myWineId) - } catch { - self.view.hideBlockingView() - errorHandler.handleNetworkError(error, in: self) - } - } + guard let wine = registerWine else { return } + self.view.showBlockingView() + Task { [weak self] in + guard let self else { return } + do { + _ = try await networkService.deleteMyWine(myWineId: wine.myWineId) + await MainActor.run { + self.view.hideBlockingView() + self.navigationController?.popViewController(animated: true) + } + } catch { + await MainActor.run { self.view.hideBlockingView() } + errorHandler.handleNetworkError(error, in: self) + } + } }Also applies to: 231-243
196-212: 가격 입력 검증이 잘못된 필드 접근과 자리수 기반 로직
priceTextField.text대신priceTextField.textField.text를 읽어야 할 가능성이 큽니다. 또한 자리수로 10억 제한을 판단하면00100000000등의 예외가 생깁니다. 정수 변환 기반으로 수정하세요.@objc func checkEmpty() { - guard let text = self.editInfoView.priceTextField.text else { - editInfoView.nextButton.isEnabled(isEnabled: false) - return - } - - if text.isEmpty { - editInfoView.nextButton.isEnabled(isEnabled: false) - } else { - editInfoView.nextButton.isEnabled(isEnabled: true) - } - - if text.count >= 10 { - showToastMessage(message: "와인 가격은 10억까지만 가능해요.", yPosition: view.frame.height * 0.5) - editInfoView.nextButton.isEnabled(isEnabled: false) - } + let raw = self.editInfoView.priceTextField.textField.text ?? "" + let sanitized = raw.replacingOccurrences(of: ",", with: "") + guard !sanitized.isEmpty, let value = Int(sanitized) else { + editInfoView.nextButton.isEnabled(isEnabled: false) + return + } + if value > 1_000_000_000 { + showToastMessage(message: "와인 가격은 10억까지만 가능해요.", yPosition: view.frame.height * 0.5) + editInfoView.nextButton.isEnabled(isEnabled: false) + return + } + editInfoView.nextButton.isEnabled(isEnabled: true) }
🧹 Nitpick comments (19)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (3)
15-23: 선택 해제(UI 초기 상태) 시 텍스트 색상 복원 누락.selectedYear가 nil일 때 라벨 텍스트만 바꾸고, 색상은 회색으로 되돌리지 않습니다. 초기 상태와의 일관성을 위해 색상도 함께 설정하세요.
} else { - selectedYearLabel.text = "빈티지 선택" + selectedYearLabel.text = "빈티지 선택" + selectedYearLabel.textColor = AppColor.gray70 }
103-105: 미사용 메서드 정리 또는 일원화.updateLabel()가 어디서도 호출되지 않습니다. didSet과 역할이 중복되므로 제거하거나, 라벨 갱신 로직을 updateLabel()로 일원화하여 재사용성을 높이세요.
89-93: 라벨 고정 width(198) 하드코딩은 잘림/현지화 이슈 위험.intrinsicContentSize를 활용하도록 우선순위를 조정하거나 trailing을 화살표와 관계로만 제약하세요.
- selectedYearLabel.snp.makeConstraints { - $0.leading.equalToSuperview().inset(16) - $0.top.bottom.equalToSuperview().inset(13.5) - $0.width.equalTo(198) - } + selectedYearLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + selectedYearLabel.snp.makeConstraints { + $0.leading.equalToSuperview().inset(16) + $0.top.bottom.equalToSuperview().inset(13.5) + $0.trailing.lessThanOrEqualTo(arrowImage.snp.leading).offset(-8) + }DE/DE/Sources/Core/CommonUI/View/CustomTextFieldView.swift (1)
96-98: 가독성 소폭 개선(nit).validationLabel.leading inset 4 → 8로 맞추면 상단 라벨과 좌우 정렬이 더 자연스럽습니다(디자인 가이드에 맞춰 선택).
DE/DE/Sources/Features/Setting/Models/MyWineViewModel.swift (1)
62-72: 표시명 헬퍼 추가 👍 + NV 처리 옵션 제안.
- getDisplayedName으로 헤더 구성 단순화된 점 좋습니다.
- 비빈티지(NV) 표기가 필요한 경우를 위해 vintage가 nil이면 “WineName NV” 형태의 옵션을 노출하는 확장도 고려해보세요(기본 동작은 현 상태 유지).
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/MyOwnedWineInfoViewController.swift (4)
30-31: viewWillAppear에서 매번 API 호출 — 불필요한 재호출 가능성.뒤로가기 후에도 항상 fetch가 재실행됩니다. 최초 진입 또는 needUpdate일 때만 호출하도록 게이트를 두면 트래픽과 깜빡임을 줄일 수 있습니다.
- fetchMyWineAPI() + if registerWine == nil || needUpdate { + fetchMyWineAPI() + needUpdate = false + }
139-146: 삭제 확인 메시지에 빈티지 반영 제안.헤더와 동일하게 getDisplayedName()을 사용하면 사용자 확인 정확도가 높아집니다.
- message: "\(currentWine.wineName)", + message: currentWine.getDisplayedName(),
195-200: @mainactor와 DispatchQueue.main 중복.setWineData가 @mainactor라면 main.async 래핑은 불필요합니다. 한 가지 방식으로 통일해 오버헤드를 줄이세요.
31-34: indicator addSubview 위치 재검토.viewWillAppear마다 addSubview하면 중복 추가 위험이 있습니다. viewDidLoad에서 1회 추가하거나, superview에 존재 여부를 체크하고 추가하세요.
DE/DE/Sources/Features/Setting/Views/MyWine/NoCountDateTopView.swift (1)
24-59: 문단 스타일 적용 범위 개선 제안.title/description에 동일 paragraphStyle(lineHeight)을 적용하고 있어 서체 크기가 다른 경우 줄간격이 어색할 수 있습니다. 파트별 paragraphStyle을 분리 적용하는 방식을 고려해보세요.
DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyWineView.swift (5)
46-54: 프로퍼티 오탈자(calender) 수정.API/검색 편의와 혼동 방지를 위해 calendar로 네이밍 정정 권장(파일 내 전부 변경 필요).
- public lazy var calender = UICalendarView().then { + public lazy var calendar = UICalendarView().then {그리고 addSubview/제약의 calender 참조도 모두 calendar로 변경해주세요.
95-99: 하단 버튼 safeArea 반영.홈 인디케이터가 있는 기기에서 버튼이 붙을 수 있습니다. safeArea에 붙여주세요.
- nextButton.snp.makeConstraints { - $0.bottom.equalToSuperview().inset(42) + nextButton.snp.makeConstraints { + $0.bottom.equalTo(self.safeAreaLayoutGuide).inset(42)
68-70: 가격 표시 포매팅(가독성 향상).세 자리 구분 기호 적용을 권장합니다.
- func setWinePrice(_ price: Int) { - self.priceTextField.textField.text = "\(price)" - } + func setWinePrice(_ price: Int) { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + self.priceTextField.textField.text = formatter.string(from: NSNumber(value: price)) + }
22-28: 숫자 패드 ‘완료’ 액세서리 제공 제안..numberPad에는 리턴 키가 없어 UX가 떨어집니다. Done 버튼이 있는 inputAccessoryView를 달아주세요.
).then { t in t.textField.keyboardType = .numberPad + let toolbar = UIToolbar() + toolbar.sizeToFit() + let done = UIBarButtonItem(title: "완료", style: .done, target: nil, action: nil) + // VC에서 외부로 target/action을 주입하거나, 클로저 콜백 형태로 노출하는 방식을 고려 + toolbar.items = [UIBarButtonItem.flexibleSpace(), done] + t.textField.inputAccessoryView = toolbar }
11-12: 프로퍼티 네이밍/접근 제어 소소한 정리.
- decsText → descText 권장(오탈자).
- 외부에서 쓰지 않는다면 private로 한정하세요(dateTitle 포함).
- let decsText = "빈티지" - let dateTitle = "구매 일자" + private let descText = "빈티지" + private let dateTitle = "구매 일자"호출부의 decsText 사용도 함께 수정 필요.
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (4)
33-39: indicator 다중 추가 방지
viewWillAppear마다indicator를 addSubview하면 중복 추가 가능성이 있습니다. 한 번만 추가되도록 가드하세요.적용 예시:
- self.view.addSubview(indicator) + if indicator.superview == nil { + self.view.addSubview(indicator) + }
74-79: 확정 시 모달 dismiss 경로 확인 필요
onYearConfirmed내부에서 본 컨트롤러에서 dismiss를 호출하지 않습니다. 모달 쪽confirmTapped에서 dismiss하는지 확인 부탁드립니다. 아니라면 아래처럼 보완하세요.- modal.onYearConfirmed = { [weak self] selectedYear in + modal.onYearConfirmed = { [weak self, weak modal] selectedYear in self?.editInfoView.yearPicker.setSelectedYear(selectedYear) self?.editInfoView.yearPicker.updatePickerView(isModalOpen: false) + modal?.dismiss(animated: true) }
102-110: bottom 제약도 safeArea로 맞추면 안전합니다홈 인디케이터 영역 침범을 피하려면 bottom도 safeArea에 붙이는 것을 권장합니다.
- editInfoView.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide) - make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview() - } + editInfoView.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.leading.trailing.equalToSuperview() + make.bottom.equalTo(view.safeAreaLayoutGuide) + }
221-229: 한국 시간대 처리 보완주석은 “시간대”지만 실제로는
locale만 설정되어 있습니다. 날짜 직렬화 일관성을 위해 KST 적용을 권장합니다.let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "ko_KR") // 한국 시간대 설정 + dateFormatter.locale = Locale(identifier: "ko_KR") + dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 한국 시간대 dateFormatter.dateFormat = "yyyy-MM-dd"
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
DE/DE/Sources/Core/CommonUI/View/CustomTextFieldView.swift(2 hunks)DE/DE/Sources/Features/Setting/Models/MyWineViewModel.swift(1 hunks)DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift(7 hunks)DE/DE/Sources/Features/Setting/ViewControllers/MyWine/MyOwnedWineInfoViewController.swift(3 hunks)DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyOwnedWineView.swift(0 hunks)DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyWineView.swift(1 hunks)DE/DE/Sources/Features/Setting/Views/MyWine/NoCountDateTopView.swift(1 hunks)DE/DE/Sources/Features/TastingNote/Views/ViewComponents/Common/MyNoteTopView.swift(1 hunks)DE/DE/Sources/Features/Vintage/YearPickerView.swift(1 hunks)
💤 Files with no reviewable changes (1)
- DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyOwnedWineView.swift
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#180
File: DE/DE/Sources/Features/TastingNote/ViewControllers/CreateVCs/RecordGraphViewController.swift:44-46
Timestamp: 2025-09-02T12:53:54.129Z
Learning: 사용자 doyeonk429는 와인 상세에서 테이스팅노트로 이동하는 플로우에서 빈티지가 무조건 설정되어 넘어간다고 설명했습니다. WineDetailViewController에서 goToTastingNote 메서드에서 vintage 가드 조건이 있어 빈티지 선택이 필수입니다.
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#0
File: :0-0
Timestamp: 2025-08-30T13:14:55.618Z
Learning: 사용자 doyeonk429는 한국어로 코드 리뷰를 받기를 선호합니다.
📚 Learning: 2025-09-02T12:53:54.129Z
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#180
File: DE/DE/Sources/Features/TastingNote/ViewControllers/CreateVCs/RecordGraphViewController.swift:44-46
Timestamp: 2025-09-02T12:53:54.129Z
Learning: 사용자 doyeonk429는 와인 상세에서 테이스팅노트로 이동하는 플로우에서 빈티지가 무조건 설정되어 넘어간다고 설명했습니다. WineDetailViewController에서 goToTastingNote 메서드에서 vintage 가드 조건이 있어 빈티지 선택이 필수입니다.
Applied to files:
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
🧬 Code graph analysis (6)
DE/DE/Sources/Features/Setting/Models/MyWineViewModel.swift (1)
DE/DE/Sources/Features/Setting/Models/MyOwnedWine.swift (1)
getVintage(132-134)
DE/DE/Sources/Features/TastingNote/Views/ViewComponents/Common/MyNoteTopView.swift (1)
DE/DE/Sources/DesignSystem/Font/TextStyle.swift (1)
apply(68-70)
DE/DE/Sources/Core/CommonUI/View/CustomTextFieldView.swift (2)
DE/DE/Sources/DesignSystem/Font/TextStyle.swift (1)
apply(68-70)DE/DE/Sources/DesignSystem/Spacing/DynamicPadding.swift (1)
dynamicValue(20-22)
DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyWineView.swift (5)
DE/DE/Sources/Core/CommonUI/Components/DividerFactory.swift (1)
make(7-11)DE/DE/Sources/Core/CommonUI/View/CustomTextFieldView.swift (1)
textField(115-132)DE/DE/Sources/Core/CommonUI/Components/CustomButton.swift (1)
isEnabled(39-42)DE/DE/Sources/DesignSystem/Font/TextStyle.swift (1)
apply(68-70)DE/DE/Sources/Core/Extensions/UIView+Extensions.swift (1)
addSubviews(78-80)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/MyOwnedWineInfoViewController.swift (4)
DE/DE/Sources/Core/CommonUI/View/SimpleListView.swift (1)
setEditButton(182-184)DE/DE/Sources/Features/Setting/Models/MyWineViewModel.swift (1)
getDisplayedName(66-72)DE/DE/Sources/Core/Extensions/UIView+Extensions.swift (1)
hideBlockingView(41-53)DE/DE/Sources/Features/NetworkErrorHandler.swift (1)
handleNetworkError(10-25)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (8)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (3)
setSelectedYear(111-114)updatePickerView(116-124)setInitalYear(54-56)DE/DE/Sources/Features/Setting/Models/MyWineViewModel.swift (1)
getVintage(62-64)DE/DE/Sources/Features/Setting/Models/MyOwnedWine.swift (1)
getVintage(132-134)DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyWineView.swift (1)
setWinePrice(68-70)DE/DE/Sources/Features/Setting/ViewControllers/MyWine/YearPickerModalViewController.swift (1)
setupUI(45-69)DE/DE/Sources/Core/Extensions/UIViewController+Extensions.swift (1)
showToastMessage(18-31)DE/DE/Sources/Core/Extensions/UIView+Extensions.swift (1)
hideBlockingView(41-53)DE/DE/Sources/Network/MyWine/Service/MyWineService.swift (1)
makeUpdateDTO(39-45)
🔇 Additional comments (8)
DE/DE/Sources/Features/TastingNote/Views/ViewComponents/Common/MyNoteTopView.swift (1)
33-33: 스타일 적용 방식 일원화 👍DesignSystem의 TextStyle.apply로 중앙집중화되어 가독성과 유지보수성이 좋아졌습니다.
DE/DE/Sources/Core/CommonUI/View/CustomTextFieldView.swift (2)
50-50: 텍스트 스타일 일원화 👍label 스타일을 DesignSystem으로 통일한 부분 좋습니다.
84-91: 좌여백/위여백 변경 확인 요청.leading 0, textField top 20으로 변경되어 기존 화면 대비 밀착/여백 증가가 있습니다. 의도된 디자인이면 OK입니다. 여러 화면에 공용으로 쓰이므로 영향 화면 확인 부탁드립니다.
DE/DE/Sources/Features/Setting/Views/MyWine/NoCountDateTopView.swift (1)
66-67: 불필요 주석/구조 정리 👍addSubview 단일화로 간결해졌습니다.
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (4)
11-11: final 클래스 전환 좋습니다상속 방지로 안정성과 최적화에 이점 있습니다.
17-17: 뷰 교체(ChangeMyWineView) 반영 LGTM하위 참조(
yearPicker,calender,priceTextField,nextButton)가 새 뷰에서 동일 인터페이스로 보입니다. 컴파일 경고만 한번 확인해주세요.
65-90: YearPicker 모달 연동 전반적으로 깔끔합니다약한 참조 처리와 PageSheet 설정 모두 적절합니다.
301-305: 모달 제스처 dismiss 대응 LGTM
presentationControllerDidDismiss에서 picker UI 상태를 원복하는 처리 좋습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (1)
15-25: 빈티지 해제 시 텍스트 컬러 복구 누락 + 초기 세팅 콜백 방지
- nil로 해제될 때 label 색상이 회색으로 복구되지 않습니다(이전 선택이 있으면 검정색이 유지됨).
- didSet에서 초기 주입까지 콜백이 불필요하게 호출될 수 있습니다(아래 플래그 활용 권장).
public private(set) var selectedYear: Int? { didSet { if let year = selectedYear { selectedYearLabel.textColor = AppColor.black selectedYearLabel.text = "\(year)" - onYearSelected?(year) + if !suppressSelectionCallback { + onYearSelected?(year) + } } else { - selectedYearLabel.text = "빈티지 선택" + selectedYearLabel.textColor = AppColor.gray70 + selectedYearLabel.text = "빈티지 선택" } } }DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (3)
145-169: 삭제 동작이 네트워크 호출 없이 단순 pop 됩니다 + 빈티지 없으면 삭제 자체가 불가
- 현재 alert의 "삭제"에서 callDeleteAPI를 호출하지 않아 서버 삭제가 이루어지지 않습니다.
- vintage가 nil이면 조기 return으로 삭제 UI 자체가 뜨지 않습니다. 메시지를 가변적으로 구성해 주세요.
@objc private func deleteNewWine() { guard let currentWine = self.registerWine else { return } - guard let vintage = currentWine.getVintage() else { return } - + let vintageText = currentWine.getVintage().map { " \($0)" } ?? "" let alert = UIAlertController( title: "이 와인을 삭제하시겠습니까?", - message: "\(currentWine.wineName) \(vintage)", + message: "\(currentWine.wineName)\(vintageText)", preferredStyle: .alert ) @@ - alert.addAction(UIAlertAction(title: "삭제", style: .destructive, handler: { [weak self] _ in + alert.addAction(UIAlertAction(title: "삭제", style: .destructive, handler: { [weak self] _ in guard let self = self else { return } self.logButtonClick(screenName: screenName, buttonName: Tracking.ButtonEvent.deleteBtnTapped, fileName: #file) - self.view.showBlockingView() - - DispatchQueue.main.async { - self.navigationController?.popViewController(animated: true) - } + self.callDeleteAPI() }))
230-241: 삭제 API 성공 시점에 UI 업데이트(로딩 해제/화면 복귀) 처리 필요성공/실패 시점에 맞춘 UI 상태 전이가 보장되어야 합니다. 메인 스레드에서 처리하세요.
- private func callDeleteAPI() { + private func callDeleteAPI() { guard let wine = registerWine else {return} self.view.showBlockingView() - Task { - do { - _ = try await networkService.deleteMyWine(myWineId: wine.myWineId) - } catch { - self.view.hideBlockingView() - errorHandler.handleNetworkError(error, in: self) - } - } + Task { [weak self] in + guard let self else { return } + do { + _ = try await networkService.deleteMyWine(myWineId: wine.myWineId) + await MainActor.run { + self.view.hideBlockingView() + self.navigationController?.popViewController(animated: true) + } + } catch { + await MainActor.run { self.view.hideBlockingView() } + errorHandler.handleNetworkError(error, in: self) + } + } }
207-210: 가격 상한 로직과 메시지 불일치(“10억까지만 가능”)현재 >= 10자리에서 막아 10억(1,000,000,000)도 입력 불가입니다. 정수 비교로 상한을 정확히 체크하세요.
- if text.count >= 10 { - showToastMessage(message: "와인 가격은 10억까지만 가능해요.", yPosition: view.frame.height * 0.5) - editInfoView.nextButton.isEnabled(isEnabled: false) - } + if let v = Int(text), v > 1_000_000_000 { + showToastMessage(message: "와인 가격은 10억까지만 가능해요.", yPosition: view.frame.height * 0.5) + editInfoView.nextButton.isEnabled(isEnabled: false) + }
♻️ Duplicate comments (2)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (1)
54-56: 초기화 API가 범위 검증을 우회하고 콜백을 트리거합니다 → 검증 경유 + 콜백 억제
- non-nil인 경우 반드시 setSelectedYear를 통해 min/max 검증을 거치세요.
- 초기 값 주입 시 onYearSelected는 호출되지 않도록 억제하세요.
- public func setInitialYear(_ year: Int?) { - self.selectedYear = year - } + public func setInitialYear(_ year: Int?) { + suppressSelectionCallback = true + defer { suppressSelectionCallback = false } + if let y = year { + setSelectedYear(y) // 범위 검증 경유 + } else { + selectedYear = nil // 플레이스홀더 표기 + } + }DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (1)
171-193: 업데이트 완료 처리: 1초 지연 pop 대신 성공 콜백 내에서 처리하세요.임의 지연은 레이스/오동작을 유발합니다(성공 전 pop, 실패 후 화면 이탈 등). 성공 시점에서 로딩 해제+pop, 실패 시 로딩만 해제.
private func completeEdit() { @@ - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.view.hideBlockingView() - self?.navigationController?.popViewController(animated: true) - } + // 성공/실패 시점 처리는 callUpdateAPI 내부에서 수행 }- private func callUpdateAPI(wineId: Int, price: Int?, vintage: Int?, buyDate: String?) { + private func callUpdateAPI(wineId: Int, price: Int?, vintage: Int?, buyDate: String?) { let data = networkService.makeUpdateDTO( buyDate: buyDate, vintage: vintage, buyPrice: price ) self.view.showBlockingView() - Task { - do { - _ = try await networkService.updateMyWine(myWineId: wineId, data: data) -// delegate?.didUpdateData(true) - } catch { - self.view.hideBlockingView() - errorHandler.handleNetworkError(error, in: self) - } - } + Task { [weak self] in + guard let self else { return } + do { + _ = try await networkService.updateMyWine(myWineId: wineId, data: data) + await MainActor.run { + self.view.hideBlockingView() + self.navigationController?.popViewController(animated: true) + } + } catch { + await MainActor.run { self.view.hideBlockingView() } + errorHandler.handleNetworkError(error, in: self) + } + } }Also applies to: 244-261
🧹 Nitpick comments (3)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (2)
8-14: 초기 세팅 콜백 억제를 위한 플래그 추가 제안초기 값 주입 시(onYearSelected 불필요 구동 방지) 임시로 콜백을 막을 수 있는 플래그가 있으면 깔끔합니다.
final class YearPickerView: UIView { public var minYear: Int public var maxYear: Int + + // 초기 세팅 시 onYearSelected 호출 억제용 + private var suppressSelectionCallback = false
103-105: 미사용 메서드 제거 또는 사용 일원화updateLabel()이 호출되지 않습니다. dead code 제거하거나 didSet에서 재사용하도록 일원화해 주세요.
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (1)
60-67: priceTextField 접근 일관성 제안setupActions에서는 textField, 검증부에서는 text를 직접 참조합니다. 한쪽으로 일원화하면 혼란과 버그를 줄일 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift(7 hunks)DE/DE/Sources/Features/Vintage/YearPickerView.swift(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#180
File: DE/DE/Sources/Features/TastingNote/ViewControllers/CreateVCs/RecordGraphViewController.swift:44-46
Timestamp: 2025-09-02T12:53:54.129Z
Learning: 사용자 doyeonk429는 와인 상세에서 테이스팅노트로 이동하는 플로우에서 빈티지가 무조건 설정되어 넘어간다고 설명했습니다. WineDetailViewController에서 goToTastingNote 메서드에서 vintage 가드 조건이 있어 빈티지 선택이 필수입니다.
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#0
File: :0-0
Timestamp: 2025-08-30T13:14:55.618Z
Learning: 사용자 doyeonk429는 한국어로 코드 리뷰를 받기를 선호합니다.
📚 Learning: 2025-09-02T12:53:54.129Z
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#180
File: DE/DE/Sources/Features/TastingNote/ViewControllers/CreateVCs/RecordGraphViewController.swift:44-46
Timestamp: 2025-09-02T12:53:54.129Z
Learning: 사용자 doyeonk429는 와인 상세에서 테이스팅노트로 이동하는 플로우에서 빈티지가 무조건 설정되어 넘어간다고 설명했습니다. WineDetailViewController에서 goToTastingNote 메서드에서 vintage 가드 조건이 있어 빈티지 선택이 필수입니다.
Applied to files:
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
📚 Learning: 2025-08-30T13:14:55.618Z
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#0
File: :0-0
Timestamp: 2025-08-30T13:14:55.618Z
Learning: 사용자 doyeonk429는 한국어로 코드 리뷰를 받기를 선호합니다.
Applied to files:
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
🧬 Code graph analysis (1)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (6)
DE/DE/Sources/Features/Vintage/YearPickerView.swift (3)
setSelectedYear(111-114)updatePickerView(116-124)setInitialYear(54-56)DE/DE/Sources/Features/Setting/Views/MyWine/ChangeMyWineView.swift (1)
setWinePrice(68-70)DE/DE/Sources/Features/Setting/Models/MyOwnedWine.swift (1)
getVintage(132-134)DE/DE/Sources/Core/Extensions/UIViewController+Extensions.swift (1)
showToastMessage(18-31)DE/DE/Sources/Core/Extensions/UIView+Extensions.swift (1)
hideBlockingView(41-53)DE/DE/Sources/Network/MyWine/Service/MyWineService.swift (1)
makeUpdateDTO(39-45)
🔇 Additional comments (1)
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (1)
176-180: 빈티지 선택 필수화 검증 필요API 계층은 vintage를 Optional로 받습니다. UX 의도대로 필수인지(또는 ‘NV’ 허용인지) 제품/기획 확인 부탁드립니다.
🚀 PR 개요
💡 PR 유형
✏️ 변경 사항 요약
🔗 관련 이슈
🧪 테스트 내역
🎨 스크린샷 또는 시연 영상 (선택)
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-09-13.at.17.29.11.mp4
✅ PR 체크리스트
develop입니다💬 추가 설명 or 리뷰 포인트 (선택)