Conversation
Walkthrough회원 탈퇴 API가 사유 문자열을 받도록 시그니처가 변경되고, 도메인 레이어의 WithdrawUseCase 및 프로토콜이 제거되었습니다. 프레젠테이션 레이어에는 탈퇴 전용 화면(ViewController, ViewModel, Model)이 추가되고, 공통 알럿 컴포넌트가 단순화되며 확인 다이얼로그가 신규 추가되었습니다. 온보딩 관련 컴포넌트는 Bitnagil 네이밍으로 일괄 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant VC as WithdrawViewController
participant VM as WithdrawViewModel
participant AR as AuthRepository
participant EP as AuthEndpoint
participant NW as NetworkClient
participant APP as AppWindow
U->>VC: 확인 토글 / 사유 선택 / 자유 입력
VC->>VM: action(input: ...)
VM-->>VC: confirm/enable 상태 Publish
U->>VC: "탈퇴하기" 탭
VC->>VM: action(input: .withdrawService)
VM->>AR: withdraw(reason: String)
AR->>EP: AuthEndpoint.withdraw(withdrawReason)
EP->>NW: POST /auth/withdraw {reasonOfWithdrawal}
NW-->>AR: Response (success/failure)
AR-->>VM: Result
alt 성공
VM-->>VC: withdrawResultPublisher(true)
VC->>VC: BitnagilConfirmDialog 표시
U->>VC: 확인
VC->>APP: 루트를 Login 흐름으로 교체
else 실패
VM-->>VC: withdrawResultPublisher(false)
VC->>VC: 버튼/상태 유지 또는 에러 처리
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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 (1)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
111-113: 액세스/리프레시 토큰 로깅은 보안 리스크토큰 값 전체를 로그로 남기면 유출 시 심각한 보안 사고로 이어집니다. 값 출력은 제거하거나 마스킹하세요.
다음과 같이 수정 제안:
- BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") - BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") - BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(userEntity.refreshToken)") + BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") + BitnagilLogger.log(logType: .debug, message: "AccessToken Saved") + BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved")
🧹 Nitpick comments (12)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
63-65: 탈퇴 사유 전송 전 최소 전처리 권장서버에 공백 포함 문자열이 그대로 전달될 수 있습니다. 트리밍 정도는 데이터 계층에서 해두면 안전합니다.
다음과 같이 적용 제안:
- func withdraw(reason: String) async throws { - let endpoint = AuthEndpoint.withdraw(withdrawReason: reason) + func withdraw(reason: String) async throws { + let sanitizedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines) + let endpoint = AuthEndpoint.withdraw(withdrawReason: sanitizedReason) _ = try await networkService.request(endpoint: endpoint, type: String.self) try tokenManager.removeToken() try removeUserInfo() }또한 서버 응답이 204(No Content)인 경우가 있다면
type: String.self는 디코딩 실패를 유발할 수 있습니다. 계약이 204라면EmptyResponseDTO.self로 맞춰주세요.Projects/Presentation/Sources/Withdraw/Model/WithdrawReason.swift (1)
8-27: 문구 하드코딩 i18n 분리 제안사용자 노출 문자열은 Localizable.strings로 분리하는 것이 좋습니다. 다국어 확장/카피 수정이 쉬워집니다.
Projects/Presentation/Sources/Setting/View/SettingView.swift (1)
229-236: fatalError 메시지 비어 있음 — 진단 가능하도록 보강빈 메시지는 추적이 어렵습니다. 어떤 의존성 해상 실패인지 명시하세요. 운영 크래시를 피하려면 fatalError 대신 로그+리턴으로의 전환도 고려해주세요.
- guard let withdrawViewModel = DIContainer.shared.resolve(type: WithdrawViewModel.self) - else { fatalError("") } + guard let withdrawViewModel = DIContainer.shared.resolve(type: WithdrawViewModel.self) + else { + BitnagilLogger.log(logType: .error, message: "WithdrawViewModel Resolve Fail: 등록되지 않은 의존성입니다.") + return + }Projects/Presentation/Sources/Common/View/BitnagilConfirmDialog.swift (2)
30-43: 프레젠테이션 스타일 명시로 일관성 확보기본 프레젠테이션이 시트로 뜨는 기기에서 배경 딤 처리/터치 차단이 깨질 수 있습니다. 컴포넌트 내부에서 기본값을 고정해 주세요.
init( title: String, message: String, confirmHandler: (() -> Void)? ) { super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + isModalInPresentation = true self.confirmHandler = confirmHandler titleLabel.text = title messageLabel.text = message
64-75: UIButton 정렬 속성 보완(nit)
titleLabel?.textAlignment = .right만으로는 버튼 콘텐츠 정렬이 보장되지 않습니다.contentHorizontalAlignment를 설정해 주세요.confirmButton.setTitle("확인", for: .normal) confirmButton.setTitleColor(BitnagilColor.orange500, for: .normal) confirmButton.titleLabel?.font = BitnagilFont(style: .body2, weight: .semiBold).font - confirmButton.titleLabel?.textAlignment = .right + confirmButton.contentHorizontalAlignment = .rightProjects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift (1)
27-27: withdraw(reason:) 호출부 반영 완료 — 빈값(공백) 검증 필요
- rg 결과: 인자 없는 호출 없음. 호출 위치: Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift:84, Projects/DataSource/Sources/Repository/AuthRepository.swift:64, Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift:73.
- 권고: UI/상위 계층(WithdrawViewModel.swift:84)에서 reason을 trimmingCharacters(in: .whitespacesAndNewlines)로 트림 후 non-empty 확인하여 호출하거나, Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift에 파라미터 설명을 추가하고 리포지토리/도메인 레벨에서 방어적 유효성(throwing/precondition 등)을 도입할 것.
Projects/Presentation/Sources/Onboarding/View/Component/BitnagilChoiceButton.swift (2)
61-64: 조건부 스타일 설정을 메서드로 추출하세요
forWithdrawChoice조건에 따른 스타일 설정이 여러 곳에 분산되어 있습니다. 가독성과 유지보수성을 위해 별도 메서드로 추출하는 것이 좋습니다.다음과 같이 리팩토링하세요:
private func configureAttribute() { backgroundColor = BitnagilColor.gray99 layer.masksToBounds = true layer.cornerRadius = Layout.cornerRadius stackView.axis = .vertical stackView.alignment = .leading stackView.spacing = Layout.stackViewSpacing stackView.isUserInteractionEnabled = false mainLabel.text = bitnagilChoice.title - mainLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font - mainLabel.textColor = BitnagilColor.gray50 - if forWithdrawChoice { - mainLabel.font = BitnagilFont(style: .subtitle1, weight: .medium).font - mainLabel.textColor = BitnagilColor.gray80 - } + configureMainLabelStyle() if let subTitle = bitnagilChoice.subTitle { subLabel = UILabel() subLabel?.text = subTitle subLabel?.font = BitnagilFont(style: .body2, weight: .medium).font subLabel?.textColor = BitnagilColor.gray50 } checkedIcon.image = BitnagilIcon.orangeCheckedCircleIcon checkedIcon.isHidden = true } +private func configureMainLabelStyle() { + if forWithdrawChoice { + mainLabel.font = BitnagilFont(style: .subtitle1, weight: .medium).font + mainLabel.textColor = BitnagilColor.gray80 + } else { + mainLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font + mainLabel.textColor = BitnagilColor.gray50 + } +}
109-114: 중복된 조건부 로직을 통합하세요
updateButtonAttribute메서드에서도forWithdrawChoice에 대한 조건부 처리가 있습니다. 이를 더 깔끔하게 정리할 수 있습니다.다음과 같이 개선하세요:
private func updateButtonAttribute() { backgroundColor = isChecked ? BitnagilColor.orange50 : BitnagilColor.gray99 - mainLabel.textColor = isChecked ? BitnagilColor.orange500 : BitnagilColor.gray50 subLabel?.textColor = isChecked ? BitnagilColor.orange500 : BitnagilColor.gray50 checkedIcon.isHidden = !isChecked + if forWithdrawChoice { let uncheckedFont = BitnagilFont(style: .subtitle1, weight: .medium).font let checkedFont = BitnagilFont(style: .body1, weight: .semiBold).font mainLabel.font = isChecked ? checkedFont : uncheckedFont mainLabel.textColor = isChecked ? BitnagilColor.orange500 : BitnagilColor.gray80 + } else { + mainLabel.textColor = isChecked ? BitnagilColor.orange500 : BitnagilColor.gray50 } }Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (1)
60-68: 탈퇴 사유 선택 로직 단순화 가능현재 로직은 먼저
nil로 설정한 후 조건부로 다시 설정하는 방식입니다. 더 직관적으로 개선할 수 있습니다.다음과 같이 단순화하세요:
private func choiceWithdrawReason(reason: WithdrawReason?) { let currentSelectedReason = withdrawReasonSubject.value - withdrawReasonSubject.send(nil) - withdrawReasonText = "" - if reason != currentSelectedReason { - withdrawReasonSubject.send(reason) - } + let newReason = (reason == currentSelectedReason) ? nil : reason + withdrawReasonSubject.send(newReason) + if newReason != nil { + withdrawReasonText = "" + } updateWithdrawButtonState() }Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift (3)
230-230: 오타 수정: comfirm → confirm파라미터 이름에 오타가 있습니다.
- .sink { [weak self] comfirm in + .sink { [weak self] confirm in self?.updateConfirmButtonState(confirm: comfirm)
280-281: 오타 수정: corfirmButtonImage → confirmButtonImage변수명에 오타가 있습니다.
- let corfirmButtonImage = confirm ? BitnagilIcon.checkedCircleSmallIcon : BitnagilIcon.uncheckedCircleSmallIcon - confirmButton.setImage(corfirmButtonImage, for: .normal) + let confirmButtonImage = confirm ? BitnagilIcon.checkedCircleSmallIcon : BitnagilIcon.uncheckedCircleSmallIcon + confirmButton.setImage(confirmButtonImage, for: .normal)
311-312: 텍스트 길이 체크 로직 최적화매번
text.count를 계산하는 대신 이미 계산된newLength를 활용할 수 있습니다.func textViewDidChange(_ textView: UITextView) { withdrawReasonTextViewPlaceholder.isHidden = !textView.text.isEmpty let reason = textView.text ?? "" viewModel.action(input: .inputWithdrawReason(reason: reason)) - let isMaxLength = textView.text.count >= withdrawReasonMaxLength + let isMaxLength = reason.count >= withdrawReasonMaxLength withdrawReasonMaxLengthLabel.isHidden = !isMaxLength }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift(2 hunks)Projects/DataSource/Sources/Repository/AuthRepository.swift(1 hunks)Projects/Domain/Sources/DomainDependencyAssembler.swift(0 hunks)Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift(1 hunks)Projects/Domain/Sources/Protocol/UseCase/WithdrawUseCaseProtocol.swift(0 hunks)Projects/Domain/Sources/UseCase/Auth/WithdrawUseCase.swift(0 hunks)Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift(1 hunks)Projects/Presentation/Sources/Common/View/BitnagilAlert.swift(3 hunks)Projects/Presentation/Sources/Common/View/BitnagilConfirmDialog.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/BitnagilChoiceProtocol.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/Component/BitnagilChoiceButton.swift(4 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift(2 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift(2 hunks)Projects/Presentation/Sources/Setting/View/SettingView.swift(2 hunks)Projects/Presentation/Sources/Setting/ViewModel/SettingViewModel.swift(0 hunks)Projects/Presentation/Sources/Withdraw/Model/WithdrawReason.swift(1 hunks)Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift(1 hunks)Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift(1 hunks)
💤 Files with no reviewable changes (4)
- Projects/Domain/Sources/DomainDependencyAssembler.swift
- Projects/Domain/Sources/Protocol/UseCase/WithdrawUseCaseProtocol.swift
- Projects/Domain/Sources/UseCase/Auth/WithdrawUseCase.swift
- Projects/Presentation/Sources/Setting/ViewModel/SettingViewModel.swift
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Applied to files:
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
🧬 Code graph analysis (8)
Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift (2)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
withdraw(63-68)Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (1)
withdraw(80-90)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (2)
register(14-16)resolve(18-25)
Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (1)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
withdraw(63-68)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (1)
withdraw(80-90)
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift (2)
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
withdraw(63-68)Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (1)
withdraw(80-90)
Projects/Presentation/Sources/Setting/View/SettingView.swift (2)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)Projects/Presentation/Sources/Setting/ViewModel/SettingViewModel.swift (2)
action(75-90)logout(114-123)
Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift (3)
Projects/Presentation/Sources/Onboarding/View/Component/BitnagilChoiceButton.swift (2)
configureAttribute(48-75)updateButtonState(117-119)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift (2)
choiceWithdrawReason(60-68)inputWithdrawReason(70-73)
Projects/Presentation/Sources/Common/View/BitnagilAlert.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(50-66)
🔇 Additional comments (11)
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift (1)
13-13: 탈퇴 사유 파라미터가 올바르게 추가되었습니다.
withdraw케이스에 탈퇴 사유를 전달하는 파라미터가 적절히 추가되었고, API 요구사항에 맞게reasonOfWithdrawal키로 전송됩니다.Also applies to: 73-74
Projects/Presentation/Sources/Onboarding/Model/BitnagilChoiceProtocol.swift (1)
2-2: 프로토콜 이름 변경이 일관되게 적용되었습니다.
OnboardingChoiceProtocol에서BitnagilChoiceProtocol로의 이름 변경이 파일명과 프로토콜 선언에 일관되게 반영되었습니다.Also applies to: 8-8
Projects/Presentation/Sources/Common/View/BitnagilAlert.swift (3)
48-48:attributedText설정이 일관성 있게 적용되었습니다.content 라벨에
attributedString을 사용하여 폰트 스타일을 적용한 것이 적절합니다. 디자인 시스템의 일관성을 위해 좋은 변경입니다.
72-76: Alert 디자인 업데이트가 v2 요구사항에 맞게 적용되었습니다.타이틀과 컨텐츠의 텍스트 스타일, 색상 및 레이아웃이 디자인 v2 요구사항에 맞게 적절히 업데이트되었습니다.
78-86: 버튼 스타일링이 디자인 시스템에 맞게 통일되었습니다.취소/확인 버튼의 배경색, 텍스트 색상, 코너 반경이 디자인 v2에 맞게 업데이트되었습니다.
Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift (1)
10-10: Extension이 새로운 프로토콜 이름으로 올바르게 업데이트되었습니다.
OnboardingChoiceTypeextension이BitnagilChoiceProtocol을 준수하도록 변경되어 프로젝트 전반의 네이밍 일관성이 유지됩니다.Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
87-87: 컴포넌트 타입 변경이 일관되게 적용되었습니다.
OnboardingChoiceButton에서BitnagilChoiceButton으로의 타입 변경이 dictionary와 button 생성 로직에 일관되게 적용되었습니다.Also applies to: 277-277
Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)
10-10: 프로토콜 전환 LGTM
BitnagilChoiceProtocol로의 일원화가 깔끔합니다. 기존 호출부와의 호환성 이슈는 없어 보입니다.Projects/Presentation/Sources/Setting/View/SettingView.swift (1)
262-274: 로그아웃 알럿 프레젠테이션 전환 LGTM
.overFullScreen설정과 새로운 카피 적용이 디자인 v2 요구사항과 일치합니다.Also applies to: 270-271
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
117-122: WithdrawViewModel DI 등록 LGTM — AuthRepositoryProtocol 등록 확인됨AuthRepositoryProtocol은 Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift:16에서 DIContainer.shared.register로 등록되어 있습니다.
Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)
32-32: BitnagilChoiceButton 전환 — 잔존 OnboardingChoiceButton 참조 없음 (확인 완료)'OnboardingChoiceButton'은 파일 헤더 주석(Projects/Presentation/Sources/Onboarding/View/Component/BitnagilChoiceButton.swift:2)에서만 발견되었습니다. 변경 승인합니다.
| viewModel.output.withdrawResultPublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { withdrawResult in | ||
| if withdrawResult { | ||
| let confirmDialog = BitnagilConfirmDialog( | ||
| title: "탈퇴가 완료되었어요", | ||
| message: "이용해 주셔서 감사합니다. 언제든 다시\n돌아오실 수 있어요:)", | ||
| confirmHandler: { | ||
| guard | ||
| let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||
| let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate, | ||
| let window = sceneDelegate.window | ||
| else { return } | ||
|
|
||
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) | ||
| else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } | ||
|
|
||
| let loginView = LoginViewController(viewModel: loginViewModel) | ||
| let navigationController = UINavigationController(rootViewController: loginView) | ||
| window?.rootViewController = navigationController | ||
| window?.makeKeyAndVisible() | ||
| }) | ||
|
|
||
| confirmDialog.modalPresentationStyle = .overFullScreen | ||
| self.present(confirmDialog, animated: false) | ||
| } | ||
| } |
There was a problem hiding this comment.
탈퇴 실패 시 사용자 피드백 누락
탈퇴 성공 시에만 UI 피드백이 있고, 실패 시 아무런 알림이 없습니다.
실패 케이스에 대한 처리를 추가하세요:
viewModel.output.withdrawResultPublisher
.receive(on: DispatchQueue.main)
.sink { withdrawResult in
if withdrawResult {
let confirmDialog = BitnagilConfirmDialog(
title: "탈퇴가 완료되었어요",
message: "이용해 주셔서 감사합니다. 언제든 다시\n돌아오실 수 있어요:)",
confirmHandler: {
// ... existing code ...
})
confirmDialog.modalPresentationStyle = .overFullScreen
self.present(confirmDialog, animated: false)
+ } else {
+ let alert = BitnagilAlert(
+ title: "탈퇴 실패",
+ message: "일시적인 오류로 탈퇴 처리에 실패했습니다.\n잠시 후 다시 시도해주세요.",
+ type: .default)
+ alert.modalPresentationStyle = .overFullScreen
+ self.present(alert, animated: false)
}
}
.store(in: &cancellables)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| viewModel.output.withdrawResultPublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { withdrawResult in | |
| if withdrawResult { | |
| let confirmDialog = BitnagilConfirmDialog( | |
| title: "탈퇴가 완료되었어요", | |
| message: "이용해 주셔서 감사합니다. 언제든 다시\n돌아오실 수 있어요:)", | |
| confirmHandler: { | |
| guard | |
| let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | |
| let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate, | |
| let window = sceneDelegate.window | |
| else { return } | |
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) | |
| else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } | |
| let loginView = LoginViewController(viewModel: loginViewModel) | |
| let navigationController = UINavigationController(rootViewController: loginView) | |
| window?.rootViewController = navigationController | |
| window?.makeKeyAndVisible() | |
| }) | |
| confirmDialog.modalPresentationStyle = .overFullScreen | |
| self.present(confirmDialog, animated: false) | |
| } | |
| } | |
| viewModel.output.withdrawResultPublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { withdrawResult in | |
| if withdrawResult { | |
| let confirmDialog = BitnagilConfirmDialog( | |
| title: "탈퇴가 완료되었어요", | |
| message: "이용해 주셔서 감사합니다. 언제든 다시\n돌아오실 수 있어요:)", | |
| confirmHandler: { | |
| guard | |
| let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | |
| let sceneDelegate = windowScene.delegate as? UIWindowSceneDelegate, | |
| let window = sceneDelegate.window | |
| else { return } | |
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) | |
| else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } | |
| let loginView = LoginViewController(viewModel: loginViewModel) | |
| let navigationController = UINavigationController(rootViewController: loginView) | |
| window?.rootViewController = navigationController | |
| window?.makeKeyAndVisible() | |
| }) | |
| confirmDialog.modalPresentationStyle = .overFullScreen | |
| self.present(confirmDialog, animated: false) | |
| } else { | |
| let alert = BitnagilAlert( | |
| title: "탈퇴 실패", | |
| message: "일시적인 오류로 탈퇴 처리에 실패했습니다.\n잠시 후 다시 시도해주세요.", | |
| type: .default) | |
| alert.modalPresentationStyle = .overFullScreen | |
| self.present(alert, animated: false) | |
| } | |
| } | |
| .store(in: &cancellables) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift
around lines 249–275, the sink only handles the success (true) path and omits
user feedback for withdrawal failures; update the subscriber to handle both
cases by adding an else branch that presents a user-facing error dialog or alert
on the main thread (e.g., a BitnagilConfirmDialog or UIAlertController) with a
clear failure message and optional retry/OK action so the user is informed when
the withdrawal fails. Ensure the presentation is performed from self and that
any dependency resolution or UI updates remain on the main queue.
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) | ||
| else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } |
There was a problem hiding this comment.
fatalError 대신 적절한 에러 처리 필요
프로덕션 환경에서 fatalError는 앱 크래시를 유발합니다. 의존성 해결 실패 시 안전한 처리가 필요합니다.
다음과 같이 수정하세요:
- guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self)
- else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") }
+ guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) else {
+ BitnagilLogger.log(logType: .error, message: "LoginViewModel 의존성 해결 실패")
+ // 에러 알림 표시
+ let alert = UIAlertController(title: "오류", message: "로그인 화면으로 이동할 수 없습니다.", preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "확인", style: .default))
+ self.present(alert, animated: true)
+ return
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) | |
| else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") } | |
| guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self) else { | |
| BitnagilLogger.log(logType: .error, message: "LoginViewModel 의존성 해결 실패") | |
| // 에러 알림 표시 | |
| let alert = UIAlertController(title: "오류", message: "로그인 화면으로 이동할 수 없습니다.", preferredStyle: .alert) | |
| alert.addAction(UIAlertAction(title: "확인", style: .default)) | |
| self.present(alert, animated: true) | |
| return | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift
around lines 263-264, the guard currently calls fatalError when DIContainer
fails to resolve LoginViewModel; replace this crash with safe error handling by:
stop using fatalError, log the resolution failure, present an appropriate
user-facing error state or fallback UI (or disable relevant features), and
return/exit the initializer or method; additionally call assertionFailure or
throw a specific error in debug builds to surface the issue during development
while preventing a production crash.
| private func withdraw() { | ||
| Task { | ||
| do { | ||
| let withdrawReason = withdrawReasonSubject.value?.title ?? withdrawReasonText | ||
| try await authRepository.withdraw(reason: withdrawReason) | ||
| withdrawResultSubject.send(true) | ||
| } catch { | ||
| withdrawResultSubject.send(false) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
탈퇴 처리 중 에러 처리 개선 필요
현재 탈퇴 실패 시 단순히 false만 전송하고 있어 사용자가 실패 이유를 알 수 없습니다. 에러 로깅과 구체적인 에러 정보 전달이 필요합니다.
다음과 같이 에러 처리를 개선하세요:
private func withdraw() {
Task {
do {
let withdrawReason = withdrawReasonSubject.value?.title ?? withdrawReasonText
try await authRepository.withdraw(reason: withdrawReason)
withdrawResultSubject.send(true)
} catch {
+ BitnagilLogger.log(logType: .error, message: "탈퇴 처리 실패: \(error)")
withdrawResultSubject.send(false)
}
}
}추가로 에러 타입별 처리를 위해 Result 타입 사용을 고려해보세요:
private let withdrawResultSubject = PassthroughSubject<Result<Bool, Error>, Never>()🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Withdraw/ViewModel/WithdrawViewModel.swift
around lines 80 to 90, the withdraw() method currently only sends a boolean on
failure which hides error details; change the withdrawResultSubject to publish
Result<Bool, Error> (e.g. PassthroughSubject<Result<Bool, Error>, Never>),
update withdraw() to catch the thrown error, log the error (with context) via
your logger, and send .success(true) on success or .failure(error) on catch so
callers receive specific error information and can handle error-type-specific
logic.
taipaise
left a comment
There was a problem hiding this comment.
오늘도 고생하셨습니다! 저번에도 느꼈지만, 추상화를 통해 객체를 다루는 구조가 정말 좋은 것 같습니다 👍👍
| self.confirmHandler = confirmHandler | ||
| titleLabel.text = title | ||
| contentLabel.text = content | ||
| contentLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: content) |
There was a problem hiding this comment.
악 이렇게 쓸 수 있다는걸 또 까먹었어요 ㅠ 분명 어디선가 attributedString을 사용했던 곳이 있었는데.. 다음번엔 꼭 잘 쓰겠습니다!!
There was a problem hiding this comment.
네네네 !! 2줄 이상부터는 attributedText를 적용해줘야 피그마처럼 보이드라구요 ~~~~ 굿굿 !!!!!!
There was a problem hiding this comment.
요 상수도 Layout으로 빼는 건 어떨깝쇼!!
There was a problem hiding this comment.
우학 !!! 확인 !! 알겟듭니다 !! 🫡 🫡 🫡
| let withdrawResultPublisher: AnyPublisher<Bool, Never> | ||
| } | ||
|
|
||
| private(set) var output: Output |
There was a problem hiding this comment.
지금 와서 드는 생각이지만, 요 output을 private(set) var로 할 필요가 있었을까요??
저도 당연히 private(Set) var로 해야지 했는데요, output을 초기화 이후에 변경하는 경우가 없으니 let으로 해도 괜찮지 않았을까? 하는 생각이 듭니다!
There was a problem hiding this comment.
허얼 ~~~~~~~~~~~~ 글게유 ........... let으로 해두 노메러이긴 할 것 같으요 ..
헐 ~~~~~~ 진짜 왜 private(set)으로 했슬까요 .....
조금 더 고민해보고 한방에 바꾸기 ? 고 ?? 어떠신지요
🌁 Background
로그아웃 BitnagilAlert의 UI를 디자인 v2로 적용하고 ~
탈퇴하고 디자인 및 API를 v2로 반영했어요 ~~
📱 Screenshot
1. 로그아웃 AlertView (
BitnagilAlert)2. 탈퇴하기 (유의사항 확인 버튼 누르기 전)
3. 탈퇴하기 (유의사항 확인 버튼 누른 후)
(테스트를 위해 잠시 maxLength 10으로 수정한 후에 캡처한 것이여요 !!!)
4. 탈퇴 확인 Dialog (BitnagilConfirmDialog)
전체 흐름
Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-09-16.at.16.35.23.mp4
👩💻 Contents
📝 Review Note
1. 탈퇴한 후 Diaglog dimmedView 터치 이벤트
BitnagilAlert에서는 dimmedView를 터치하면 dismiss하도록 이벤트가 들어가 있던데
BitnagilComfirmDialog에서 해당 이벤트를 넣어버리면 탈퇴한 후에도 탈퇴하기 뷰에 머물러버려서 일단 dimmedView를 터치해도 아무 이벤트가 발생하지 않도록 했습니당 ~~
📣 Related Issue
Summary by CodeRabbit
New Features
Style
Improvements