-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature/#186] 보유와인 정보 수정화면 > 빈티지 적용 #187
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
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 상태를 원복하는 처리 좋습니다.
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
Outdated
Show resolved
Hide resolved
DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
Show resolved
Hide resolved
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.
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 리뷰 포인트 (선택)