Skip to content

Conversation

@doyeonk429
Copy link
Contributor

🚀 PR 개요

보유와인 정보 수정화면 > 빈티지 적용

💡 PR 유형

  • ✨ Feature (기능 추가)
  • 🐞 Bugfix (버그 수정)
  • 🔥 Hotfix (긴급 수정)
  • 🔧 Refactor (코드 리팩토링)
  • ⚙️ Chore (환경 설정)
  • 📝 Docs (문서 작성 및 수정)

✏️ 변경 사항 요약

  • 관련 컴포넌트 레이아웃/스타일 수정
  • 보유와인 수정 뷰 리팩토링
  • 보유와인 수정 뷰컨트롤러 리팩토링
  • 보유와인 정보 조회 버그 수정

🔗 관련 이슈

🧪 테스트 내역

  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

🎨 스크린샷 또는 시연 영상 (선택)

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-09-13.at.17.29.11.mp4

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다
  • Merge 대상 Branch가 develop입니다
  • PR 제목이 컨벤션에 맞습니다
  • 관련 이슈 번호를 작성했습니다
  • 기능이 정상적으로 작동합니다
  • 불필요한 코드를 제거했습니다

💬 추가 설명 or 리뷰 포인트 (선택)

리뷰어가 중점적으로 봐야 할 부분이나 설명이 필요한 내용을 자유롭게 작성해주세요.

@doyeonk429 doyeonk429 self-assigned this Sep 13, 2025
@doyeonk429 doyeonk429 requested a review from dlguszoo September 13, 2025 08:37
@doyeonk429 doyeonk429 changed the title Feature/#186 보유와인 정보 수정화면 > 빈티지 적용 [Feature/#186] 보유와인 정보 수정화면 > 빈티지 적용 Sep 13, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • 소유 와인 편집 화면을 개편하여 스크롤 가능한 폼과 연도(빈티지) 선택기를 추가했습니다. 기존 빈티지가 초기 표시되며 저장 동작이 개선되었습니다.
    • 와인 이름에 빈티지가 자동으로 함께 표시됩니다.
  • Refactor

    • 편집 화면 구조를 전면 교체하고 데이터 로딩 타이밍을 조정해 안정성과 응답성을 개선했습니다.
    • 모달 시트 동작 및 해제 처리 로직을 정리하여 선택 상태 동기화를 강화했습니다.
  • Style

    • 텍스트 스타일을 일원화하고 여백/간격을 조정해 가독성을 높였습니다.

Walkthrough

빈티지(연도) 선택 기능을 UI 전반에 연동하고 보유 와인 수정 화면을 재구성했습니다. 기존 ChangeMyOwnedWineView를 삭제하고 ChangeMyWineView로 교체했으며, 뷰모델·컨트롤러·YearPicker API와 텍스트 스타일링 및 일부 레이아웃을 정리·조정했습니다.

Changes

Cohort / File(s) Change Summary
뷰 교체: 보유와인 수정 UI 삭제
DE/.../Views/MyWine/ChangeMyOwnedWineView.swift
기존 ChangeMyOwnedWineView 파일 삭제.
신규: 수정용 뷰 추가
DE/.../Views/MyWine/ChangeMyWineView.swift
ChangeMyWineView 추가(스크롤형 폼, topView, YearPickerView, 가격 필드, 캘린더, 저장 버튼). 공개 API setTopSection(name:), setWinePrice(_:) 등 추가.
컨트롤러 변경: 빈티지 연동·API 시그니처
DE/.../ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift
클래스 final화, 뷰 타입을 ChangeMyWineView로 변경, YearPicker 모달 연동 및 sheet delegate 처리 추가, callUpdateAPIvintage 파라미터 추가, 빈티지 필수 체크/토스트, 삭제 메시지에 빈티지 포함, UI 타이밍 조정.
상세화면: 데이터 로드·UI 업데이트 안전화
DE/.../ViewControllers/MyWine/MyOwnedWineInfoViewController.swift
fetchMyWineAPI()viewWillAppear로 이동, 액션 바인딩 분리, @MainActor/DispatchQueue.main 사용으로 UI 업데이트 안전화, header에 getDisplayedName() 사용.
뷰모델 확장: 빈티지/표시명 접근자 추가
DE/.../Models/MyWineViewModel.swift
public func getVintage() -> Int?, public func getDisplayedName() -> String 추가.
YearPicker API 추가
DE/.../Features/Vintage/YearPickerView.swift
public func setInitialYear(_ year: Int?) 추가(선택 연도 직접 설정, optional 허용).
프레젠테이션 위임 변경
DE/.../Features/Vintage/ReusableVintageSelectionViewController.swift
sheetPresentationController.delegate 할당으로 변경 및 UISheetPresentationControllerDelegate 추가.
공통 텍스트 스타일 통일 및 레이아웃 조정
DE/.../CommonUI/View/CustomTextFieldView.swift, DE/.../TastingNote/.../MyNoteTopView.swift
개별 폰트/색 지정 제거하고 AppTextStyle.*.apply(...)로 스타일 중앙화; 일부 여백(offset/inset) 조정.
소규모 뷰 정리
DE/.../Views/MyWine/NoCountDateTopView.swift
주석/미사용 라벨 제거, 단일 titleLabel로 단순화.

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
Loading
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())
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • yeseonglee
  • dlguszoo

Poem

빈티지 숫자 하나, 모달을 열고 건네면 🍷
새 뷰는 스크롤 속에 숨 쉬고, 제목엔 연도도 함께 빛나네
저장 누르면 기록은 고요히 바뀌고, 코드도 한결 정돈되었네 ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[Feature/#186] 보유와인 정보 수정화면 > 빈티지 적용"은 이 PR의 핵심 변경점인 '보유와인 수정 화면에 빈티지 적용'을 명확히 요약하고 이슈 라벨/번호 규칙을 따르므로 간결하고 적절합니다. 스캔하는 동료가 주요 목적을 빠르게 파악할 수 있습니다.
Linked Issues Check ✅ Passed 연결된 이슈 #186의 요구사항(뷰 리디자인, 빈티지 셀렉 연동, API 수정)은 대체로 충족됩니다: ChangeMyWineView 추가 및 기존 뷰 삭제로 리디자인을 반영했고 YearPicker/모달 연동 및 setInitialYear 추가로 빈티지 선택이 통합되었으며 callUpdateAPI 시그니처와 DTO 전달로 vintage 파라미터가 업데이트 요청에 포함되었습니다. 다만 YearPickerView.setInitialYear가 내부 범위 검증(min/max)을 우회하는 점은 추가 검토가 필요합니다.
Out of Scope Changes Check ✅ Passed 변경사항 대부분은 이슈 범위(뷰 리디자인, 빈티지 통합, 관련 스타일/레이아웃 정리)에 부합합니다. 다만 공통 스타일 리팩터링(CustomTextFieldView, MyNoteTopView)과 ChangeMyOwnedWineView 삭제는 전역 영향 가능성이 있으므로 삭제된 타입의 다른 참조가 없는지와 연관된 통합 테스트 결과를 확인할 필요가 있습니다.
Description Check ✅ Passed PR 설명에는 목적, 변경 요약, 테스트 내역, 관련 이슈(#186) 등이 잘 정리되어 있어 변경 세트와 관련성이 명확하며 이 체크의 널리 허용된 기준을 충족합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch design/issue-186

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ebec51 and e8834f5.

📒 Files selected for processing (2)
  • DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift (7 hunks)
  • DE/DE/Sources/Features/Vintage/ReusableVintageSelectionViewController.swift (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.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-13T09:09:45.616Z
Learnt from: doyeonk429
PR: Drink-Easy/DE_iOS#187
File: DE/DE/Sources/Features/Setting/ViewControllers/MyWine/ChangeMyOwnedWineViewController.swift:86-90
Timestamp: 2025-09-13T09:09:45.616Z
Learning: 사용자 doyeonk429는 UISheetPresentationControllerDelegate를 추가로 채택하고 sheet.delegate = self로 설정하는 방식을 선호합니다. 이는 presentationController?.delegate 설정보다 더 안전하고 명시적인 방법입니다.

Applied to files:

  • DE/DE/Sources/Features/Vintage/ReusableVintageSelectionViewController.swift
🔇 Additional comments (2)
DE/DE/Sources/Features/Vintage/ReusableVintageSelectionViewController.swift (2)

88-88: sheet.delegate = self 전환 굿 — 팀 러닝과 일치

UISheetPresentationControllerDelegate를 채택하고 sheet.delegate = self로 설정한 방향이 더 명시적이고 안전합니다. 이전 PR 러닝과도 일치합니다.


82-89: iOS 15 가드 추가 및 배포 타깃 확인 필요

sheetPresentationController / UISheetPresentationControllerDelegate는 iOS 15+ API입니다. 리포지토리에서 IPHONEOS_DEPLOYMENT_TARGET을 찾을 수 없어(스크립트 결과 비어있음) 배포 타깃을 확인하세요. 15 미만을 지원하면 아래 가드+폴백을 적용하세요.

File: DE/DE/Sources/Features/Vintage/ReusableVintageSelectionViewController.swift (around lines 82–89)

-            modal.modalPresentationStyle = .pageSheet
-            
-            if let sheet = modal.sheetPresentationController {
-                sheet.detents = [.medium()]
-                sheet.prefersGrabberVisible = true
-                sheet.prefersScrollingExpandsWhenScrolledToEdge = false
-                sheet.delegate = self
-            }
+            modal.modalPresentationStyle = .pageSheet
+
+            if #available(iOS 15.0, *) {
+                if let sheet = modal.sheetPresentationController {
+                    sheet.detents = [.medium()]
+                    sheet.prefersGrabberVisible = true
+                    sheet.prefersScrollingExpandsWhenScrolledToEdge = false
+                    sheet.delegate = self
+                }
+            } else {
+                modal.presentationController?.delegate = self
+            }

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 53ea6e0 and bf68ca8.

📒 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 상태를 원복하는 처리 좋습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between bf68ca8 and 5ebec51.

📒 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’ 허용인지) 제품/기획 확인 부탁드립니다.

@doyeonk429 doyeonk429 merged commit bc36453 into develop Sep 13, 2025
1 check passed
@doyeonk429 doyeonk429 deleted the design/issue-186 branch September 13, 2025 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Design] 보유와인 수정 UI 변경

2 participants