Skip to content

[Feat-T3-185] 로그아웃 · 탈퇴 기능 디자인 및 서버 v2 적용#63

Merged
choijungp merged 7 commits intodevelopfrom
fix/withdraw
Sep 18, 2025
Merged

[Feat-T3-185] 로그아웃 · 탈퇴 기능 디자인 및 서버 v2 적용#63
choijungp merged 7 commits intodevelopfrom
fix/withdraw

Conversation

@choijungp
Copy link
Contributor

@choijungp choijungp commented Sep 16, 2025

🌁 Background

로그아웃 BitnagilAlert의 UI를 디자인 v2로 적용하고 ~
탈퇴하고 디자인 및 API를 v2로 반영했어요 ~~


📱 Screenshot

1. 로그아웃 AlertView (BitnagilAlert)

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-16 at 16 01 23 Simulator Screenshot - iPhone 13 mini - 2025-09-16 at 15 54 36 Simulator Screenshot - iPhone 16 Pro - 2025-09-16 at 15 57 18

2. 탈퇴하기 (유의사항 확인 버튼 누르기 전)

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-16 at 16 28 59 Simulator Screenshot - iPhone 13 mini - 2025-09-16 at 15 54 40 Simulator Screenshot - iPhone 16 Pro - 2025-09-16 at 15 57 33

3. 탈퇴하기 (유의사항 확인 버튼 누른 후)

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-16 at 16 26 34 Simulator Screenshot - iPhone 13 mini - 2025-09-16 at 15 54 42 Simulator Screenshot - iPhone 16 Pro - 2025-09-16 at 15 57 36

(테스트를 위해 잠시 maxLength 10으로 수정한 후에 캡처한 것이여요 !!!)


4. 탈퇴 확인 Dialog (BitnagilConfirmDialog)

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-09-16 at 16 29 09 Simulator Screenshot - iPhone 13 mini - 2025-09-16 at 15 55 08 Simulator Screenshot - iPhone 16 Pro - 2025-09-16 at 15 57 41

전체 흐름

Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-09-16.at.16.35.23.mp4

👩‍💻 Contents

  • 로그아웃 Alert 디자인 수정
  • 탈퇴하기 디자인 수정 및 api 연동

📝 Review Note

1. 탈퇴한 후 Diaglog dimmedView 터치 이벤트

BitnagilAlert에서는 dimmedView를 터치하면 dismiss하도록 이벤트가 들어가 있던데
BitnagilComfirmDialog에서 해당 이벤트를 넣어버리면 탈퇴한 후에도 탈퇴하기 뷰에 머물러버려서 일단 dimmedView를 터치해도 아무 이벤트가 발생하지 않도록 했습니당 ~~

📣 Related Issue

  • close #T3-185

Summary by CodeRabbit

  • New Features

    • 설정에서 ‘회원탈퇴’ 전용 화면 추가: 사유 선택(목록), 직접 입력(최대 100자), 확인 체크 후 탈퇴 활성화, 탈퇴 성공 시 로그인 화면으로 이동
    • 탈퇴 사유 옵션(여러가지) 추가 및 선택 UI 제공
    • 새 확인 다이얼로그(확인 버튼 1개) 도입
  • Style

    • 알림 팝업 디자인 단일화: 이미지 제거·텍스트 중심·타이포·컬러·모서리 조정
    • 선택 버튼 컴포넌트에 탈퇴용 스타일 추가
  • Improvements

    • 설정의 로그아웃 문구·표현 업데이트
    • 설정에서 바로 탈퇴 화면으로 이동하는 흐름으로 간소화

@choijungp choijungp requested a review from taipaise September 16, 2025 07:37
@choijungp choijungp self-assigned this Sep 16, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 16, 2025

Walkthrough

회원 탈퇴 API가 사유 문자열을 받도록 시그니처가 변경되고, 도메인 레이어의 WithdrawUseCase 및 프로토콜이 제거되었습니다. 프레젠테이션 레이어에는 탈퇴 전용 화면(ViewController, ViewModel, Model)이 추가되고, 공통 알럿 컴포넌트가 단순화되며 확인 다이얼로그가 신규 추가되었습니다. 온보딩 관련 컴포넌트는 Bitnagil 네이밍으로 일괄 변경되었습니다.

Changes

Cohort / File(s) Summary
DataSource: Withdraw API 사유 전달
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift, Projects/DataSource/Sources/Repository/AuthRepository.swift, Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift
탈퇴 API가 사유 문자열을 요구하도록 변경: AuthEndpoint.withdraw(withdrawReason:) 도입, bodyParameters에 reasonOfWithdrawal 추가. AuthRepository.withdraw(reason:) 및 프로토콜 시그니처 갱신.
Domain: Withdraw UseCase 제거
Projects/Domain/Sources/Protocol/UseCase/WithdrawUseCaseProtocol.swift (삭제), Projects/Domain/Sources/UseCase/Auth/WithdrawUseCase.swift (삭제), Projects/Domain/Sources/DomainDependencyAssembler.swift
WithdrawUseCase 프로토콜 및 구현 삭제, DI 등록 제거.
Presentation DI 등록 추가
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
WithdrawViewModel DI 등록 추가(의존성으로 AuthRepositoryProtocol 사용).
Common UI: Alert 개편 및 ConfirmDialog 추가
Projects/Presentation/Sources/Common/View/BitnagilAlert.swift, Projects/Presentation/Sources/Common/View/BitnagilConfirmDialog.swift
BitnagilAlert에서 이미지 분기 제거 및 단일 텍스트형으로 리팩터링(이니셜라이저 시그니처 변경). 신규 BitnagilConfirmDialog 추가(단일 확인 버튼 모달).
Onboarding 컴포넌트 네이밍 변경
Projects/Presentation/Sources/Onboarding/Model/BitnagilChoiceProtocol.swift, .../Onboarding/Model/OnboardingChoiceType.swift, .../Onboarding/Model/RecommendedRoutine.swift, .../Onboarding/View/Component/BitnagilChoiceButton.swift, .../Onboarding/View/OnboardingViewController.swift, Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
OnboardingChoiceProtocolBitnagilChoiceProtocol로 리네임. 선택 버튼 클래스 OnboardingChoiceButtonBitnagilChoiceButton으로 변경 및 생성자 시그니처(탈퇴용 플래그) 업데이트. 사용처 타입 교체.
Setting 화면 흐름 조정
Projects/Presentation/Sources/Setting/View/SettingView.swift, .../Setting/ViewModel/SettingViewModel.swift
로그아웃 알럿 호출부를 개편된 BitnagilAlert로 수정. 탈퇴는 기존 알럿 제거 후 Withdraw 화면으로 네비게이션하도록 변경. SettingViewModel에서 탈퇴 관련 입력/처리 제거.
Withdraw 기능 추가
Projects/Presentation/Sources/Withdraw/Model/WithdrawReason.swift, .../Withdraw/View/WithdrawViewController.swift, .../Withdraw/ViewModel/WithdrawViewModel.swift
탈퇴 사유 모델(enum) 추가, 탈퇴 UI(ViewController) 및 로직(ViewModel) 도입. 확인 체크 → 사유 선택 또는 자유입력(100자 제한) → API 호출 → 성공 시 로그인 화면으로 전환 흐름 구현.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[Feat-T3-185] 로그아웃 · 탈퇴 기능 디자인 및 서버 v2 적용"은 PR의 주요 변경점(로그아웃 알림 디자인 수정, 탈퇴 화면·뷰모델 추가 및 탈퇴 API v2로의 변경 — 사유 전달 추가)을 명확하게 요약합니다; 변경된 파일 목록(raw_summary)에 나타난 BitnagilAlert/BitnagilConfirmDialog, WithdrawViewController/WithdrawViewModel, AuthEndpoint/AuthRepository/AuthRepositoryProtocol 등의 수정과 일치하므로 PR의 주된 목적을 잘 전달합니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/withdraw

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a113383 and a17c7d1.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Presentation/Sources/Withdraw/View/WithdrawViewController.swift

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ 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 (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 = .right
Projects/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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d827ba and a113383.

📒 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이 새로운 프로토콜 이름으로 올바르게 업데이트되었습니다.

OnboardingChoiceType extension이 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)에서만 발견되었습니다. 변경 승인합니다.

Comment on lines +249 to +275
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)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

탈퇴 실패 시 사용자 피드백 누락

탈퇴 성공 시에만 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.

Suggested change
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.

Comment on lines +263 to +264
guard let loginViewModel = DIContainer.shared.resolve(type: LoginViewModel.self)
else { fatalError("loginViewModel 의존성이 등록되지 않았습니다.") }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +80 to +90
private func withdraw() {
Task {
do {
let withdrawReason = withdrawReasonSubject.value?.title ?? withdrawReasonText
try await authRepository.withdraw(reason: withdrawReason)
withdrawResultSubject.send(true)
} catch {
withdrawResultSubject.send(false)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

탈퇴 처리 중 에러 처리 개선 필요

현재 탈퇴 실패 시 단순히 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.

Copy link
Collaborator

@taipaise taipaise left a comment

Choose a reason for hiding this comment

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

오늘도 고생하셨습니다! 저번에도 느꼈지만, 추상화를 통해 객체를 다루는 구조가 정말 좋은 것 같습니다 👍👍

self.confirmHandler = confirmHandler
titleLabel.text = title
contentLabel.text = content
contentLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: content)
Copy link
Collaborator

Choose a reason for hiding this comment

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

악 이렇게 쓸 수 있다는걸 또 까먹었어요 ㅠ 분명 어디선가 attributedString을 사용했던 곳이 있었는데.. 다음번엔 꼭 잘 쓰겠습니다!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네네네 !! 2줄 이상부터는 attributedText를 적용해줘야 피그마처럼 보이드라구요 ~~~~ 굿굿 !!!!!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

요 상수도 Layout으로 빼는 건 어떨깝쇼!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

우학 !!! 확인 !! 알겟듭니다 !! 🫡 🫡 🫡

let withdrawResultPublisher: AnyPublisher<Bool, Never>
}

private(set) var output: Output
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금 와서 드는 생각이지만, 요 output을 private(set) var로 할 필요가 있었을까요??
저도 당연히 private(Set) var로 해야지 했는데요, output을 초기화 이후에 변경하는 경우가 없으니 let으로 해도 괜찮지 않았을까? 하는 생각이 듭니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

허얼 ~~~~~~~~~~~~ 글게유 ........... let으로 해두 노메러이긴 할 것 같으요 ..
헐 ~~~~~~ 진짜 왜 private(set)으로 했슬까요 .....

조금 더 고민해보고 한방에 바꾸기 ? 고 ?? 어떠신지요

@choijungp choijungp merged commit 5ad2984 into develop Sep 18, 2025
2 checks passed
@choijungp choijungp deleted the fix/withdraw branch September 18, 2025 08:42
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.

2 participants

Comments